├── (webpack) └── buildin │ ├── global.js │ └── module.js ├── README.md ├── node_modules ├── @cryptography │ └── aes │ │ └── dist │ │ └── es │ │ └── aes.js ├── async-mutex │ └── lib │ │ ├── Mutex.js │ │ └── index.js ├── base64-js │ └── index.js ├── big-integer │ └── BigInteger.js ├── idb-keyval │ └── dist │ │ └── esm │ │ └── index.js ├── ieee754 │ └── index.js ├── isarray │ └── index.js ├── node-libs-browser │ └── node_modules │ │ └── buffer │ │ └── index.js ├── opus-recorder │ └── dist │ │ ├── decoderWorker.min.js │ │ ├── encoderWorker.min.js │ │ └── waveWorker.min.js ├── os-browserify │ └── browser.js ├── pako │ └── dist │ │ └── pako_inflate.js ├── process │ └── browser.js ├── qr-creator │ └── dist │ │ └── qr-creator.es6.min.js └── websocket │ └── lib │ ├── browser.js │ └── version.js ├── src ├── App.tsx ├── api │ ├── gramjs │ │ ├── apiBuilders │ │ │ ├── bots.ts │ │ │ ├── chats.ts │ │ │ ├── common.ts │ │ │ ├── helpers.ts │ │ │ ├── messages.ts │ │ │ ├── misc.ts │ │ │ ├── pathBytesToSvg.ts │ │ │ ├── payments.ts │ │ │ ├── peers.ts │ │ │ ├── symbols.ts │ │ │ └── users.ts │ │ ├── gramjsBuilders │ │ │ └── index.ts │ │ ├── helpers.ts │ │ ├── localDb.ts │ │ ├── methods │ │ │ ├── auth.ts │ │ │ ├── bots.ts │ │ │ ├── chats.ts │ │ │ ├── client.ts │ │ │ ├── management.ts │ │ │ ├── media.ts │ │ │ ├── messages.ts │ │ │ ├── payments.ts │ │ │ ├── settings.ts │ │ │ ├── symbols.ts │ │ │ ├── twoFaSettings.ts │ │ │ └── users.ts │ │ ├── provider.ts │ │ ├── updater.ts │ │ └── worker │ │ │ ├── provider.ts │ │ │ └── worker.ts │ └── types │ │ ├── index.ts │ │ ├── media.ts │ │ └── messages.ts ├── assets │ ├── DiscussionGroupsDucks.tgs │ ├── FoldersAll.tgs │ ├── FoldersNew.tgs │ ├── TwoFactorSetupMonkeyClose.tgs │ ├── TwoFactorSetupMonkeyIdle.tgs │ ├── TwoFactorSetupMonkeyPeek.tgs │ ├── TwoFactorSetupMonkeyTracking.tgs │ ├── app-inactive.png │ ├── fonts │ │ └── roboto.css │ ├── mastercard.svg │ ├── monkey.svg │ ├── telegram-logo.svg │ └── visa.svg ├── bundles │ └── main.ts ├── components │ ├── auth │ │ ├── Auth.scss │ │ ├── Auth.tsx │ │ ├── AuthCode.async.tsx │ │ ├── AuthPassword.async.tsx │ │ ├── AuthPhoneNumber.tsx │ │ ├── AuthQrCode.tsx │ │ ├── AuthRegister.async.tsx │ │ ├── CountryCodeInput.scss │ │ ├── CountryCodeInput.tsx │ │ └── helpers │ │ │ └── getSuggestedLanguage.ts │ ├── common │ │ ├── AnimatedEmoji.scss │ │ ├── AnimatedEmoji.tsx │ │ ├── AnimatedSticker.tsx │ │ ├── Audio.scss │ │ ├── Audio.tsx │ │ ├── Avatar.scss │ │ ├── Avatar.tsx │ │ ├── CalendarModal.async.tsx │ │ ├── CalendarModal.scss │ │ ├── CalendarModal.tsx │ │ ├── ChatLink.tsx │ │ ├── DeleteChatModal.scss │ │ ├── DeleteChatModal.tsx │ │ ├── DeleteMessageModal.async.tsx │ │ ├── DeleteMessageModal.tsx │ │ ├── Document.tsx │ │ ├── EmbeddedMessage.scss │ │ ├── EmbeddedMessage.tsx │ │ ├── File.scss │ │ ├── File.tsx │ │ ├── GifButton.scss │ │ ├── GifButton.tsx │ │ ├── GroupChatInfo.tsx │ │ ├── LastMessageMeta.scss │ │ ├── LastMessageMeta.tsx │ │ ├── Media.scss │ │ ├── Media.tsx │ │ ├── MessageLink.tsx │ │ ├── MessageOutgoingStatus.scss │ │ ├── MessageOutgoingStatus.tsx │ │ ├── NothingFound.scss │ │ ├── NothingFound.tsx │ │ ├── PasswordForm.tsx │ │ ├── PasswordMonkey.scss │ │ ├── PasswordMonkey.tsx │ │ ├── Picker.scss │ │ ├── Picker.tsx │ │ ├── PickerSelectedItem.scss │ │ ├── PickerSelectedItem.tsx │ │ ├── PinMessageModal.tsx │ │ ├── PrivateChatInfo.tsx │ │ ├── ReportMessageModal.tsx │ │ ├── SafeLink.tsx │ │ ├── StickerButton.scss │ │ ├── StickerButton.tsx │ │ ├── StickerSetModal.async.tsx │ │ ├── StickerSetModal.scss │ │ ├── StickerSetModal.tsx │ │ ├── TypingStatus.scss │ │ ├── TypingStatus.tsx │ │ ├── UiLoader.scss │ │ ├── UiLoader.tsx │ │ ├── UnpinAllMessagesModal.async.tsx │ │ ├── UnpinAllMessagesModal.tsx │ │ ├── UserLink.tsx │ │ ├── UsernameInput.tsx │ │ ├── VerifiedIcon.scss │ │ ├── VerifiedIcon.tsx │ │ ├── WebLink.scss │ │ ├── WebLink.tsx │ │ └── helpers │ │ │ ├── animatedAssets.ts │ │ │ ├── detectCardType.ts │ │ │ ├── documentInfo.ts │ │ │ ├── mediaDimensions.ts │ │ │ ├── parseEmojiOnlyString.ts │ │ │ ├── renderActionMessageText.tsx │ │ │ ├── renderMessageText.tsx │ │ │ ├── renderText.tsx │ │ │ └── waveform.ts │ ├── left │ │ ├── ArchivedChats.async.tsx │ │ ├── ArchivedChats.scss │ │ ├── ArchivedChats.tsx │ │ ├── ConnectionState.scss │ │ ├── ConnectionState.tsx │ │ ├── LeftColumn.scss │ │ ├── LeftColumn.tsx │ │ ├── NewChatButton.scss │ │ ├── NewChatButton.tsx │ │ ├── main │ │ │ ├── Badge.scss │ │ │ ├── Badge.tsx │ │ │ ├── Chat.scss │ │ │ ├── Chat.tsx │ │ │ ├── ChatFolders.tsx │ │ │ ├── ChatList.tsx │ │ │ ├── ContactList.async.tsx │ │ │ ├── ContactList.tsx │ │ │ ├── EmptyFolder.scss │ │ │ ├── EmptyFolder.tsx │ │ │ ├── LeftMain.scss │ │ │ ├── LeftMain.tsx │ │ │ ├── LeftMainHeader.scss │ │ │ ├── LeftMainHeader.tsx │ │ │ └── hooks │ │ │ │ └── useChatAnimationType.ts │ │ ├── newChat │ │ │ ├── NewChat.async.tsx │ │ │ ├── NewChat.scss │ │ │ ├── NewChat.tsx │ │ │ ├── NewChatStep1.tsx │ │ │ └── NewChatStep2.tsx │ │ ├── search │ │ │ ├── AudioResults.tsx │ │ │ ├── ChatMessage.scss │ │ │ ├── ChatMessage.tsx │ │ │ ├── ChatMessageResults.tsx │ │ │ ├── ChatResults.tsx │ │ │ ├── DateSuggest.scss │ │ │ ├── DateSuggest.tsx │ │ │ ├── FileResults.tsx │ │ │ ├── LeftSearch.async.tsx │ │ │ ├── LeftSearch.scss │ │ │ ├── LeftSearch.tsx │ │ │ ├── LeftSearchResultChat.tsx │ │ │ ├── LinkResults.tsx │ │ │ ├── MediaResults.tsx │ │ │ ├── RecentContacts.scss │ │ │ ├── RecentContacts.tsx │ │ │ └── helpers │ │ │ │ ├── createMapStateToProps.ts │ │ │ │ └── getSenderName.ts │ │ └── settings │ │ │ ├── Settings.async.tsx │ │ │ ├── Settings.scss │ │ │ ├── Settings.tsx │ │ │ ├── SettingsEditProfile.tsx │ │ │ ├── SettingsGeneral.tsx │ │ │ ├── SettingsGeneralBackground.scss │ │ │ ├── SettingsGeneralBackground.tsx │ │ │ ├── SettingsGeneralBackgroundColor.scss │ │ │ ├── SettingsGeneralBackgroundColor.tsx │ │ │ ├── SettingsHeader.tsx │ │ │ ├── SettingsLanguage.tsx │ │ │ ├── SettingsMain.tsx │ │ │ ├── SettingsNotifications.tsx │ │ │ ├── SettingsPrivacy.tsx │ │ │ ├── SettingsPrivacyActiveSessions.tsx │ │ │ ├── SettingsPrivacyBlockedUsers.tsx │ │ │ ├── SettingsPrivacyVisibility.tsx │ │ │ ├── SettingsPrivacyVisibilityExceptionList.tsx │ │ │ ├── SettingsStickerSet.scss │ │ │ ├── SettingsStickerSet.tsx │ │ │ ├── WallpaperTile.scss │ │ │ ├── WallpaperTile.tsx │ │ │ ├── folders │ │ │ ├── SettingsFolders.scss │ │ │ ├── SettingsFolders.tsx │ │ │ ├── SettingsFoldersChatFilters.tsx │ │ │ ├── SettingsFoldersChatsPicker.scss │ │ │ ├── SettingsFoldersChatsPicker.tsx │ │ │ ├── SettingsFoldersEdit.tsx │ │ │ └── SettingsFoldersMain.tsx │ │ │ ├── helper │ │ │ └── privacy.ts │ │ │ └── twoFa │ │ │ ├── SettingsTwoFa.tsx │ │ │ ├── SettingsTwoFaCongratulations.tsx │ │ │ ├── SettingsTwoFaEmailCode.tsx │ │ │ ├── SettingsTwoFaEnabled.tsx │ │ │ ├── SettingsTwoFaPassword.tsx │ │ │ ├── SettingsTwoFaSkippableForm.tsx │ │ │ └── SettingsTwoFaStart.tsx │ ├── main │ │ ├── AppInactive.scss │ │ ├── AppInactive.tsx │ │ ├── Dialogs.async.tsx │ │ ├── Dialogs.scss │ │ ├── Dialogs.tsx │ │ ├── ForwardPicker.async.tsx │ │ ├── ForwardPicker.scss │ │ ├── ForwardPicker.tsx │ │ ├── HistoryCalendar.async.tsx │ │ ├── HistoryCalendar.tsx │ │ ├── Main.async.tsx │ │ ├── Main.scss │ │ ├── Main.tsx │ │ ├── Notifications.async.tsx │ │ ├── Notifications.tsx │ │ ├── SafeLinkModal.async.tsx │ │ └── SafeLinkModal.tsx │ ├── mediaViewer │ │ ├── MediaViewer.async.tsx │ │ ├── MediaViewer.scss │ │ ├── MediaViewer.tsx │ │ ├── MediaViewerActions.scss │ │ ├── MediaViewerActions.tsx │ │ ├── MediaViewerFooter.scss │ │ ├── MediaViewerFooter.tsx │ │ ├── PanZoom.scss │ │ ├── PanZoom.tsx │ │ ├── SenderInfo.scss │ │ ├── SenderInfo.tsx │ │ ├── VideoPlayer.scss │ │ ├── VideoPlayer.tsx │ │ ├── VideoPlayerControls.scss │ │ ├── VideoPlayerControls.tsx │ │ ├── ZoomControls.scss │ │ ├── ZoomControls.tsx │ │ └── helpers │ │ │ ├── formatFileSize.ts │ │ │ └── ghostAnimation.ts │ ├── middle │ │ ├── ActionMessage.tsx │ │ ├── AudioPlayer.scss │ │ ├── AudioPlayer.tsx │ │ ├── ContactGreeting.scss │ │ ├── ContactGreeting.tsx │ │ ├── DeleteSelectedMessageModal.tsx │ │ ├── HeaderActions.tsx │ │ ├── HeaderMenuContainer.async.tsx │ │ ├── HeaderMenuContainer.scss │ │ ├── HeaderMenuContainer.tsx │ │ ├── HeaderPinnedMessage.tsx │ │ ├── MessageList.scss │ │ ├── MessageList.tsx │ │ ├── MessageListContent.tsx │ │ ├── MessageSelectToolbar.async.tsx │ │ ├── MessageSelectToolbar.scss │ │ ├── MessageSelectToolbar.tsx │ │ ├── MiddleColumn.scss │ │ ├── MiddleColumn.tsx │ │ ├── MiddleHeader.scss │ │ ├── MiddleHeader.tsx │ │ ├── MobileSearch.async.tsx │ │ ├── MobileSearch.scss │ │ ├── MobileSearch.tsx │ │ ├── NoMessages.scss │ │ ├── NoMessages.tsx │ │ ├── PinnedMessageNavigation.tsx │ │ ├── ScrollDownButton.scss │ │ ├── ScrollDownButton.tsx │ │ ├── composer │ │ │ ├── AttachMenu.async.tsx │ │ │ ├── AttachMenu.scss │ │ │ ├── AttachMenu.tsx │ │ │ ├── AttachmentModal.async.tsx │ │ │ ├── AttachmentModal.scss │ │ │ ├── AttachmentModal.tsx │ │ │ ├── BotKeyboardMenu.scss │ │ │ ├── BotKeyboardMenu.tsx │ │ │ ├── Composer.scss │ │ │ ├── Composer.tsx │ │ │ ├── ComposerEmbeddedMessage.scss │ │ │ ├── ComposerEmbeddedMessage.tsx │ │ │ ├── CustomSendMenu.async.tsx │ │ │ ├── CustomSendMenu.scss │ │ │ ├── CustomSendMenu.tsx │ │ │ ├── DropArea.async.tsx │ │ │ ├── DropArea.scss │ │ │ ├── DropArea.tsx │ │ │ ├── DropTarget.scss │ │ │ ├── DropTarget.tsx │ │ │ ├── EmojiButton.scss │ │ │ ├── EmojiButton.tsx │ │ │ ├── EmojiCategory.tsx │ │ │ ├── EmojiPicker.scss │ │ │ ├── EmojiPicker.tsx │ │ │ ├── EmojiTooltip.async.tsx │ │ │ ├── EmojiTooltip.scss │ │ │ ├── EmojiTooltip.tsx │ │ │ ├── GifPicker.scss │ │ │ ├── GifPicker.tsx │ │ │ ├── InlineBotTooltip.async.tsx │ │ │ ├── InlineBotTooltip.scss │ │ │ ├── InlineBotTooltip.tsx │ │ │ ├── MentionTooltip.async.tsx │ │ │ ├── MentionTooltip.scss │ │ │ ├── MentionTooltip.tsx │ │ │ ├── MessageInput.tsx │ │ │ ├── PollModal.async.tsx │ │ │ ├── PollModal.scss │ │ │ ├── PollModal.tsx │ │ │ ├── StickerPicker.scss │ │ │ ├── StickerPicker.tsx │ │ │ ├── StickerSet.tsx │ │ │ ├── StickerSetCover.tsx │ │ │ ├── StickerSetCoverAnimated.tsx │ │ │ ├── StickerTooltip.async.tsx │ │ │ ├── StickerTooltip.scss │ │ │ ├── StickerTooltip.tsx │ │ │ ├── SymbolMenu.async.tsx │ │ │ ├── SymbolMenu.scss │ │ │ ├── SymbolMenu.tsx │ │ │ ├── SymbolMenuFooter.tsx │ │ │ ├── TextFormatter.scss │ │ │ ├── TextFormatter.tsx │ │ │ ├── WebPagePreview.scss │ │ │ ├── WebPagePreview.tsx │ │ │ ├── helpers │ │ │ │ ├── applyIosAutoCapitalizationFix.ts │ │ │ │ ├── buildAttachment.ts │ │ │ │ ├── getMessageTextAsHtml.ts │ │ │ │ ├── parseMessageInput.ts │ │ │ │ ├── searchUserName.ts │ │ │ │ └── selection.ts │ │ │ ├── hooks │ │ │ │ ├── useClipboardPaste.ts │ │ │ │ ├── useDraft.ts │ │ │ │ ├── useEditing.ts │ │ │ │ ├── useEmojiTooltip.ts │ │ │ │ ├── useInlineBotTooltip.ts │ │ │ │ ├── useMentionTooltip.ts │ │ │ │ ├── useStickerTooltip.ts │ │ │ │ └── useVoiceRecording.ts │ │ │ └── inlineResults │ │ │ │ ├── ArticleResult.tsx │ │ │ │ ├── BaseResult.scss │ │ │ │ ├── BaseResult.tsx │ │ │ │ ├── GifResult.tsx │ │ │ │ ├── MediaResult.scss │ │ │ │ ├── MediaResult.tsx │ │ │ │ └── StickerResult.tsx │ │ ├── helpers │ │ │ ├── calculateMiddleFooterTransforms.ts │ │ │ ├── getCurrencySign.ts │ │ │ ├── groupMessages.ts │ │ │ ├── inputFormatters.ts │ │ │ └── preventMessageInputBlur.ts │ │ ├── hooks │ │ │ ├── useMessageObservers.ts │ │ │ ├── useScrollHooks.ts │ │ │ └── useStickyDates.ts │ │ └── message │ │ │ ├── Album.scss │ │ │ ├── Album.tsx │ │ │ ├── CommentButton.scss │ │ │ ├── CommentButton.tsx │ │ │ ├── Contact.scss │ │ │ ├── Contact.tsx │ │ │ ├── ContextMenuContainer.async.tsx │ │ │ ├── ContextMenuContainer.tsx │ │ │ ├── InlineButtons.scss │ │ │ ├── InlineButtons.tsx │ │ │ ├── Invoice.scss │ │ │ ├── Invoice.tsx │ │ │ ├── MentionLink.tsx │ │ │ ├── Message.scss │ │ │ ├── Message.tsx │ │ │ ├── MessageContextMenu.scss │ │ │ ├── MessageContextMenu.tsx │ │ │ ├── MessageMeta.scss │ │ │ ├── MessageMeta.tsx │ │ │ ├── Photo.tsx │ │ │ ├── Poll.scss │ │ │ ├── Poll.tsx │ │ │ ├── PollOption.scss │ │ │ ├── PollOption.tsx │ │ │ ├── RoundVideo.scss │ │ │ ├── RoundVideo.tsx │ │ │ ├── Sticker.scss │ │ │ ├── Sticker.tsx │ │ │ ├── Video.tsx │ │ │ ├── WebPage.scss │ │ │ ├── WebPage.tsx │ │ │ ├── helpers │ │ │ ├── buildContentClassName.ts │ │ │ ├── calculateAlbumLayout.ts │ │ │ ├── calculateAuthorWidth.ts │ │ │ ├── copyOptions.ts │ │ │ ├── getCustomAppendixBg.ts │ │ │ └── mediaDimensions.ts │ │ │ ├── hocs │ │ │ └── withSelectControl.tsx │ │ │ └── hooks │ │ │ ├── useBlurredMediaThumbRef.ts │ │ │ ├── useFocusMessage.ts │ │ │ └── usePauseOnInactive.ts │ ├── payment │ │ ├── CardInput.scss │ │ ├── CardInput.tsx │ │ ├── Checkout.scss │ │ ├── Checkout.tsx │ │ ├── ExpiryInput.tsx │ │ ├── PaymentInfo.scss │ │ ├── PaymentInfo.tsx │ │ ├── PaymentModal.async.tsx │ │ ├── PaymentModal.scss │ │ ├── PaymentModal.tsx │ │ ├── ReceiptModal.async.tsx │ │ ├── ReceiptModal.tsx │ │ ├── Shipping.scss │ │ ├── Shipping.tsx │ │ ├── ShippingInfo.scss │ │ └── ShippingInfo.tsx │ ├── right │ │ ├── AddChatMembers.scss │ │ ├── AddChatMembers.tsx │ │ ├── ChatExtra.tsx │ │ ├── DeleteMemberModal.tsx │ │ ├── GifSearch.async.tsx │ │ ├── GifSearch.scss │ │ ├── GifSearch.tsx │ │ ├── PollAnswerResults.scss │ │ ├── PollAnswerResults.tsx │ │ ├── PollResults.async.tsx │ │ ├── PollResults.scss │ │ ├── PollResults.tsx │ │ ├── Profile.scss │ │ ├── Profile.tsx │ │ ├── ProfileInfo.scss │ │ ├── ProfileInfo.tsx │ │ ├── ProfilePhoto.scss │ │ ├── ProfilePhoto.tsx │ │ ├── RightColumn.scss │ │ ├── RightColumn.tsx │ │ ├── RightHeader.scss │ │ ├── RightHeader.tsx │ │ ├── RightSearch.async.tsx │ │ ├── RightSearch.scss │ │ ├── RightSearch.tsx │ │ ├── StickerSearch.async.tsx │ │ ├── StickerSearch.scss │ │ ├── StickerSearch.tsx │ │ ├── StickerSetResult.tsx │ │ ├── hooks │ │ │ ├── useAsyncRendering.ts │ │ │ ├── usePhotosPreload.ts │ │ │ ├── useProfileState.ts │ │ │ ├── useProfileViewportIds.ts │ │ │ └── useTransitionFixes.ts │ │ └── management │ │ │ ├── ManageChannel.tsx │ │ │ ├── ManageChatAdministrators.tsx │ │ │ ├── ManageChatPrivacyType.tsx │ │ │ ├── ManageDiscussion.tsx │ │ │ ├── ManageGroup.tsx │ │ │ ├── ManageGroupAdminRights.tsx │ │ │ ├── ManageGroupMembers.tsx │ │ │ ├── ManageGroupPermissions.tsx │ │ │ ├── ManageGroupRecentActions.tsx │ │ │ ├── ManageGroupRemovedUsers.tsx │ │ │ ├── ManageGroupUserPermissions.tsx │ │ │ ├── ManageGroupUserPermissionsCreate.tsx │ │ │ ├── ManageUser.tsx │ │ │ ├── Management.async.tsx │ │ │ ├── Management.scss │ │ │ └── Management.tsx │ └── ui │ │ ├── AvatarEditable.scss │ │ ├── AvatarEditable.tsx │ │ ├── Button.scss │ │ ├── Button.tsx │ │ ├── Checkbox.scss │ │ ├── Checkbox.tsx │ │ ├── CheckboxGroup.tsx │ │ ├── ConfirmDialog.tsx │ │ ├── CropModal.scss │ │ ├── CropModal.tsx │ │ ├── DropdownMenu.scss │ │ ├── DropdownMenu.tsx │ │ ├── FloatingActionButton.scss │ │ ├── FloatingActionButton.tsx │ │ ├── InfiniteScroll.tsx │ │ ├── InputText.tsx │ │ ├── Link.scss │ │ ├── Link.tsx │ │ ├── ListItem.scss │ │ ├── ListItem.tsx │ │ ├── Loading.scss │ │ ├── Loading.tsx │ │ ├── Menu.scss │ │ ├── Menu.tsx │ │ ├── MenuItem.scss │ │ ├── MenuItem.tsx │ │ ├── Modal.scss │ │ ├── Modal.tsx │ │ ├── Notification.scss │ │ ├── Notification.tsx │ │ ├── Portal.ts │ │ ├── ProgressSpinner.scss │ │ ├── ProgressSpinner.tsx │ │ ├── Radio.scss │ │ ├── Radio.tsx │ │ ├── RadioGroup.tsx │ │ ├── RangeSlider.scss │ │ ├── RangeSlider.tsx │ │ ├── ResponsiveHoverButton.tsx │ │ ├── RippleEffect.scss │ │ ├── RippleEffect.tsx │ │ ├── SearchInput.scss │ │ ├── SearchInput.tsx │ │ ├── Select.tsx │ │ ├── ShowMoreButton.scss │ │ ├── ShowMoreButton.tsx │ │ ├── ShowTransition.tsx │ │ ├── Spinner.scss │ │ ├── Spinner.tsx │ │ ├── Switcher.scss │ │ ├── Switcher.tsx │ │ ├── Tab.scss │ │ ├── Tab.tsx │ │ ├── TabList.scss │ │ ├── TabList.tsx │ │ ├── Transition.scss │ │ └── Transition.tsx ├── config.ts ├── global │ ├── cache.ts │ ├── index.ts │ └── initial.ts ├── hooks │ ├── reducers │ │ ├── useFoldersReducer.ts │ │ ├── usePaymentReducer.ts │ │ └── useTwoFaReducer.ts │ ├── useAudioPlayer.ts │ ├── useBackgroundMode.ts │ ├── useBeforeUnload.ts │ ├── useBlur.ts │ ├── useBlurSync.ts │ ├── useBrowserOnline.ts │ ├── useBuffering.ts │ ├── useCacheBuster.ts │ ├── useCanvasBlur.ts │ ├── useChatContextActions.ts │ ├── useContextMenuHandlers.ts │ ├── useContextMenuPosition.ts │ ├── useCurrentOrPrev.ts │ ├── useCustomBackground.ts │ ├── useDebounce.ts │ ├── useEffectWithPrevDeps.ts │ ├── useEnsureMessage.ts │ ├── useFlag.ts │ ├── useFocusAfterAnimation.tsx │ ├── useForceUpdate.ts │ ├── useFullscreen.ts │ ├── useHeavyAnimationCheck.ts │ ├── useHeavyAnimationCheckForVideo.ts │ ├── useHistoryBack.ts │ ├── useHorizontalScroll.ts │ ├── useInfiniteScroll.ts │ ├── useIntersectionObserver.ts │ ├── useKeyboardListNavigation.ts │ ├── useLang.ts │ ├── useLangString.ts │ ├── useLayoutEffectWithPrevDeps.ts │ ├── useMedia.ts │ ├── useMediaDownload.ts │ ├── useMediaWithDownloadProgress.ts │ ├── useModuleLoader.ts │ ├── useMouseInside.ts │ ├── useOnChange.ts │ ├── usePrevDuringAnimation.ts │ ├── usePrevious.ts │ ├── useReducer.ts │ ├── useSelectWithEnter.ts │ ├── useShowTransition.ts │ ├── useThrottle.ts │ ├── useThrottledMemo.ts │ ├── useTransitionForMedia.ts │ ├── useVideoCleanup.ts │ ├── useVirtualBackdrop.ts │ ├── useWebpThumbnail.ts │ └── useWindowSize.ts ├── index.tsx ├── lib │ ├── fastBlur.js │ ├── gramjs │ │ ├── Helpers.js │ │ ├── Password.js │ │ ├── Utils.js │ │ ├── Version.js │ │ ├── client │ │ │ ├── 2fa.ts │ │ │ ├── TelegramClient.js │ │ │ ├── auth.ts │ │ │ ├── downloadFile.ts │ │ │ └── uploadFile.ts │ │ ├── crypto │ │ │ ├── AuthKey.js │ │ │ ├── CTR.js │ │ │ ├── Factorizator.js │ │ │ ├── IGE.js │ │ │ ├── RSA.ts │ │ │ ├── converters.ts │ │ │ ├── crypto.js │ │ │ └── words.ts │ │ ├── errors │ │ │ ├── Common.js │ │ │ ├── RPCBaseErrors.js │ │ │ ├── RPCErrorList.js │ │ │ └── index.js │ │ ├── events │ │ │ ├── Raw.js │ │ │ ├── common.js │ │ │ └── index.js │ │ ├── extensions │ │ │ ├── AsyncQueue.js │ │ │ ├── BinaryReader.js │ │ │ ├── BinaryWriter.js │ │ │ ├── Logger.js │ │ │ ├── MessagePacker.js │ │ │ ├── PromisedWebSockets.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── network │ │ │ ├── Authenticator.ts │ │ │ ├── MTProtoPlainSender.js │ │ │ ├── MTProtoSender.js │ │ │ ├── MTProtoState.js │ │ │ ├── RequestState.js │ │ │ ├── connection │ │ │ │ ├── Connection.js │ │ │ │ ├── TCPAbridged.js │ │ │ │ ├── TCPObfuscated.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── sessions │ │ │ ├── Abstract.js │ │ │ ├── CacheApiSession.js │ │ │ ├── CallbackSession.js │ │ │ ├── IdbSession.js │ │ │ ├── LocalStorageSession.js │ │ │ ├── Memory.js │ │ │ ├── StorageSession.js │ │ │ ├── StringSession.js │ │ │ └── index.js │ │ └── tl │ │ │ ├── AllTLObjects.js │ │ │ ├── api.js │ │ │ ├── apiTl.js │ │ │ ├── core │ │ │ ├── GZIPPacked.js │ │ │ ├── MessageContainer.js │ │ │ ├── RPCResult.js │ │ │ ├── TLMessage.js │ │ │ └── index.js │ │ │ ├── generationHelpers.js │ │ │ ├── index.js │ │ │ └── schemaTl.js │ ├── punycode.js │ ├── rlottie │ │ ├── RLottie.ts │ │ └── rlottie.worker.ts │ ├── teact │ │ ├── dom-events.ts │ │ ├── teact-dom.ts │ │ ├── teact.ts │ │ └── teactn.tsx │ ├── twemojiRegex.js │ └── webp │ │ └── webp_wasm.worker.js ├── modules │ ├── actions │ │ ├── api │ │ │ ├── bots.ts │ │ │ ├── chats.ts │ │ │ ├── globalSearch.ts │ │ │ ├── initial.ts │ │ │ ├── localSearch.ts │ │ │ ├── management.ts │ │ │ ├── messages.ts │ │ │ ├── payments.ts │ │ │ ├── settings.ts │ │ │ ├── symbols.ts │ │ │ ├── sync.ts │ │ │ ├── twoFaSettings.ts │ │ │ └── users.ts │ │ ├── apiUpdaters │ │ │ ├── chats.ts │ │ │ ├── initial.ts │ │ │ ├── messages.ts │ │ │ ├── misc.ts │ │ │ ├── settings.ts │ │ │ ├── symbols.ts │ │ │ ├── twoFaSettings.ts │ │ │ └── users.ts │ │ └── ui │ │ │ ├── chats.ts │ │ │ ├── globalSearch.ts │ │ │ ├── initial.ts │ │ │ ├── localSearch.ts │ │ │ ├── messages.ts │ │ │ ├── misc.ts │ │ │ ├── payments.ts │ │ │ ├── settings.ts │ │ │ ├── stickerSearch.ts │ │ │ └── users.ts │ ├── helpers │ │ ├── chats.ts │ │ ├── localSearch.ts │ │ ├── messageMedia.ts │ │ ├── messages.ts │ │ ├── payments.ts │ │ └── users.ts │ ├── reducers │ │ ├── bots.ts │ │ ├── chats.ts │ │ ├── globalSearch.ts │ │ ├── localSearch.ts │ │ ├── management.ts │ │ ├── messages.ts │ │ ├── payments.ts │ │ ├── settings.ts │ │ ├── symbols.ts │ │ ├── twoFaSettings.ts │ │ └── users.ts │ └── selectors │ │ ├── chats.ts │ │ ├── globalSearch.ts │ │ ├── localSearch.ts │ │ ├── management.ts │ │ ├── messages.ts │ │ ├── payments.ts │ │ ├── settings.ts │ │ ├── symbols.ts │ │ ├── ui.ts │ │ └── users.ts ├── serviceWorker.ts ├── serviceWorker │ ├── assetCache.ts │ ├── progressive.ts │ └── pushNotification.ts ├── styles │ └── index.scss ├── types │ └── index.ts └── util │ ├── WorkerConnector.ts │ ├── activeTabMonitor.ts │ ├── animation.ts │ ├── appBadge.ts │ ├── arePropsShallowEqual.ts │ ├── audioPlayer.ts │ ├── buildClassName.ts │ ├── cacheApi.ts │ ├── callbacks.ts │ ├── captureEscKeyListener.ts │ ├── captureEvents.ts │ ├── captureKeyboardListeners.ts │ ├── clipboard.ts │ ├── colors.ts │ ├── countries.ts │ ├── createWorkerInterface.ts │ ├── cssAnimationEndListeners.ts │ ├── cycleRestrict.ts │ ├── dateFormat.ts │ ├── deeplink.ts │ ├── deleteLastCharacterOutsideSelection.ts │ ├── download.ts │ ├── emoji.ts │ ├── environment.ts │ ├── environmentSystemTheme.ts │ ├── environmentWebp.ts │ ├── fastSmoothScroll.ts │ ├── fastSmoothScrollHorizontal.ts │ ├── files.ts │ ├── findInViewport.ts │ ├── focusEditableElement.ts │ ├── fonts.ts │ ├── generateIdFor.ts │ ├── getReadableErrorText.ts │ ├── handleError.ts │ ├── insertHtmlInSelection.ts │ ├── isFullyVisible.ts │ ├── iteratees.ts │ ├── langProvider.ts │ ├── mediaLoader.ts │ ├── memo.ts │ ├── moduleLoader.ts │ ├── notifications.ts │ ├── oggToWav.ts │ ├── patchSafariProgressiveAudio.ts │ ├── phoneNumber.ts │ ├── requestQuery.ts │ ├── resetScroll.ts │ ├── routing.ts │ ├── safePlay.ts │ ├── schedulers.ts │ ├── scrollLock.ts │ ├── searchWords.ts │ ├── sessions.ts │ ├── setTooltipItemVisible.ts │ ├── setupServiceWorker.ts │ ├── switchTheme.ts │ ├── systemFilesDialog.ts │ ├── textFormat.ts │ ├── trapFocus.ts │ ├── trimText.ts │ ├── voiceRecording.ts │ ├── waveform.ts │ ├── webpToPng.ts │ ├── websync.ts │ └── windowSize.ts └── webpack └── bootstrap.html /(webpack)/buildin/global.js: -------------------------------------------------------------------------------- 1 | var g; 2 | 3 | // This works in non-strict mode 4 | g = (function() { 5 | return this; 6 | })(); 7 | 8 | try { 9 | // This works if eval is allowed (see CSP) 10 | g = g || new Function("return this")(); 11 | } catch (e) { 12 | // This works if the window reference is available 13 | if (typeof window === "object") g = window; 14 | } 15 | 16 | // g can still be undefined, but nothing to do about it... 17 | // We return undefined, instead of nothing here, so it's 18 | // easier to handle this case. if(!global) { ...} 19 | 20 | module.exports = g; 21 | -------------------------------------------------------------------------------- /(webpack)/buildin/module.js: -------------------------------------------------------------------------------- 1 | module.exports = function(module) { 2 | if (!module.webpackPolyfill) { 3 | module.deprecate = function() {}; 4 | module.paths = []; 5 | // module.parent = undefined by default 6 | if (!module.children) module.children = []; 7 | Object.defineProperty(module, "loaded", { 8 | enumerable: true, 9 | get: function() { 10 | return module.l; 11 | } 12 | }); 13 | Object.defineProperty(module, "id", { 14 | enumerable: true, 15 | get: function() { 16 | return module.i; 17 | } 18 | }); 19 | module.webpackPolyfill = 1; 20 | } 21 | return module; 22 | }; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram web code base (for studying only) 2 | 3 | This repo contains (some) code from https://web.telegram.org - by Telegram. We're not write or update anything on this, it's original code that downloaded through Chrome debuger tool. 4 | 5 | **Cong Nguyen** -------------------------------------------------------------------------------- /node_modules/async-mutex/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var Mutex_1 = require("./Mutex"); 4 | exports.Mutex = Mutex_1.default; 5 | -------------------------------------------------------------------------------- /node_modules/isarray/index.js: -------------------------------------------------------------------------------- 1 | var toString = {}.toString; 2 | 3 | module.exports = Array.isArray || function (arr) { 4 | return toString.call(arr) == '[object Array]'; 5 | }; 6 | -------------------------------------------------------------------------------- /node_modules/opus-recorder/dist/decoderWorker.min.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return new Worker(__webpack_public_path__ + "5054ce745024de60a724.worker.js"); 3 | }; -------------------------------------------------------------------------------- /node_modules/opus-recorder/dist/encoderWorker.min.js: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "c0155344d336103c2b6a0b28cc510750.js"; -------------------------------------------------------------------------------- /node_modules/opus-recorder/dist/waveWorker.min.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return new Worker(__webpack_public_path__ + "695e66b225ec96107343.worker.js"); 3 | }; -------------------------------------------------------------------------------- /node_modules/os-browserify/browser.js: -------------------------------------------------------------------------------- 1 | exports.endianness = function () { return 'LE' }; 2 | 3 | exports.hostname = function () { 4 | if (typeof location !== 'undefined') { 5 | return location.hostname 6 | } 7 | else return ''; 8 | }; 9 | 10 | exports.loadavg = function () { return [] }; 11 | 12 | exports.uptime = function () { return 0 }; 13 | 14 | exports.freemem = function () { 15 | return Number.MAX_VALUE; 16 | }; 17 | 18 | exports.totalmem = function () { 19 | return Number.MAX_VALUE; 20 | }; 21 | 22 | exports.cpus = function () { return [] }; 23 | 24 | exports.type = function () { return 'Browser' }; 25 | 26 | exports.release = function () { 27 | if (typeof navigator !== 'undefined') { 28 | return navigator.appVersion; 29 | } 30 | return ''; 31 | }; 32 | 33 | exports.networkInterfaces 34 | = exports.getNetworkInterfaces 35 | = function () { return {} }; 36 | 37 | exports.arch = function () { return 'javascript' }; 38 | 39 | exports.platform = function () { return 'browser' }; 40 | 41 | exports.tmpdir = exports.tmpDir = function () { 42 | return '/tmp'; 43 | }; 44 | 45 | exports.EOL = '\n'; 46 | 47 | exports.homedir = function () { 48 | return '/' 49 | }; 50 | -------------------------------------------------------------------------------- /node_modules/websocket/lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../package.json').version; 2 | -------------------------------------------------------------------------------- /src/api/gramjs/apiBuilders/helpers.ts: -------------------------------------------------------------------------------- 1 | export function bytesToDataUri(bytes: Buffer, shouldOmitPrefix = false, mimeType: string = 'image/jpeg') { 2 | const prefix = shouldOmitPrefix ? '' : `data:${mimeType};base64,`; 3 | 4 | return `${prefix}${btoa(String.fromCharCode(...bytes))}`; 5 | } 6 | 7 | export function omitVirtualClassFields(instance: any) { 8 | if (!instance) { 9 | return undefined; 10 | } 11 | 12 | const { 13 | flags, 14 | CONSTRUCTOR_ID, 15 | SUBCLASS_OF_ID, 16 | className, 17 | classType, 18 | ...rest 19 | } = instance; 20 | 21 | return rest; 22 | } 23 | -------------------------------------------------------------------------------- /src/api/gramjs/apiBuilders/pathBytesToSvg.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 3 | // eslint-disable-next-line max-len 4 | const TEMPLATE = ''; 5 | const LOOKUP = 'AACAAAAHAAALMAAAQASTAVAAAZaacaaaahaaalmaaaqastava.az0123456789-,'; 6 | 7 | export function pathBytesToSvg(bytes: Buffer, width: number, height: number) { 8 | return TEMPLATE 9 | .replace('{{path}}', buildPath(bytes)) 10 | .replace('{{width}}', String(width)) 11 | .replace('{{height}}', String(height)); 12 | } 13 | 14 | function buildPath(bytes: Buffer) { 15 | let path = 'M'; 16 | 17 | const len = bytes.length; 18 | for (let i = 0; i < len; i++) { 19 | const num = bytes[i]; 20 | if (num >= 128 + 64) { 21 | path += LOOKUP[num - 128 - 64]; 22 | } else { 23 | if (num >= 128) { 24 | path += ','; 25 | } else if (num >= 64) { 26 | path += '-'; 27 | } 28 | path += String(num & 63); 29 | } 30 | } 31 | 32 | path += 'z'; 33 | 34 | return path; 35 | } 36 | -------------------------------------------------------------------------------- /src/api/gramjs/apiBuilders/peers.ts: -------------------------------------------------------------------------------- 1 | import { Api as GramJs } from '../../../lib/gramjs'; 2 | 3 | export function isPeerUser(peer: GramJs.TypePeer): peer is GramJs.PeerUser { 4 | return peer.hasOwnProperty('userId'); 5 | } 6 | 7 | export function isPeerChat(peer: GramJs.TypePeer): peer is GramJs.PeerChat { 8 | return peer.hasOwnProperty('chatId'); 9 | } 10 | 11 | export function isPeerChannel(peer: GramJs.TypePeer): peer is GramJs.PeerChannel { 12 | return peer.hasOwnProperty('channelId'); 13 | } 14 | 15 | export function isInputPeerUser(peer: GramJs.TypeInputPeer): peer is GramJs.InputPeerUser { 16 | return peer.hasOwnProperty('userId'); 17 | } 18 | 19 | export function isInputPeerChat(peer: GramJs.TypeInputPeer): peer is GramJs.InputPeerChat { 20 | return peer.hasOwnProperty('chatId'); 21 | } 22 | 23 | export function isInputPeerChannel(peer: GramJs.TypeInputPeer): peer is GramJs.InputPeerChannel { 24 | return peer.hasOwnProperty('channelId'); 25 | } 26 | -------------------------------------------------------------------------------- /src/api/gramjs/localDb.ts: -------------------------------------------------------------------------------- 1 | import { Api as GramJs } from '../../lib/gramjs'; 2 | import { ApiMessage } from '../types'; 3 | 4 | interface LocalDb { 5 | localMessages: Record; 6 | // Used for loading avatars and media through in-memory Gram JS instances. 7 | chats: Record; 8 | users: Record; 9 | messages: Record; 10 | documents: Record; 11 | stickerSets: Record; 12 | photos: Record; 13 | webDocuments: Record; 14 | } 15 | 16 | export default { 17 | localMessages: {}, 18 | chats: {}, 19 | users: {}, 20 | messages: {}, 21 | documents: {}, 22 | stickerSets: {}, 23 | photos: {}, 24 | webDocuments: {}, 25 | } as LocalDb; 26 | -------------------------------------------------------------------------------- /src/api/gramjs/worker/worker.ts: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return new Worker(__webpack_public_path__ + "f892866691880f4589e1.worker.js"); 3 | }; -------------------------------------------------------------------------------- /src/api/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users'; 2 | export * from './chats'; 3 | export * from './messages'; 4 | export * from './updates'; 5 | export * from './media'; 6 | export * from './payments'; 7 | export * from './settings'; 8 | export * from './bots'; 9 | export * from './misc'; 10 | -------------------------------------------------------------------------------- /src/api/types/media.ts: -------------------------------------------------------------------------------- 1 | // We cache avatars as Data URI for faster initial load 2 | // and messages media as Blob for smaller size. 3 | 4 | export enum ApiMediaFormat { 5 | DataUri, 6 | BlobUrl, 7 | Lottie, 8 | Progressive, 9 | Stream, 10 | } 11 | 12 | export type ApiParsedMedia = string | Blob | AnyLiteral | ArrayBuffer; 13 | export type ApiPreparedMedia = string | AnyLiteral; 14 | export type ApiMediaFormatToPrepared = T extends ApiMediaFormat.Lottie ? AnyLiteral : string; 15 | -------------------------------------------------------------------------------- /src/assets/DiscussionGroupsDucks.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "DiscussionGroupsDucks.9ea453d1be9d1b0ee77a992f8e587485.tgs"; -------------------------------------------------------------------------------- /src/assets/FoldersAll.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "FoldersAll.3f9f9e243d19f0fbf9aaaff11cbd4572.tgs"; -------------------------------------------------------------------------------- /src/assets/FoldersNew.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "FoldersNew.9a40d71c0c8be70f5bd14ff2d7bc1593.tgs"; -------------------------------------------------------------------------------- /src/assets/TwoFactorSetupMonkeyClose.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "TwoFactorSetupMonkeyClose.604c4c833d322b7e6c3ea19bef058241.tgs"; -------------------------------------------------------------------------------- /src/assets/TwoFactorSetupMonkeyIdle.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "TwoFactorSetupMonkeyIdle.dea4a492c144df84ddab778dc8a3f0cd.tgs"; -------------------------------------------------------------------------------- /src/assets/TwoFactorSetupMonkeyPeek.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "TwoFactorSetupMonkeyPeek.1905436b042520363d7e59f5d7f903ab.tgs"; -------------------------------------------------------------------------------- /src/assets/TwoFactorSetupMonkeyTracking.tgs: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "TwoFactorSetupMonkeyTracking.eb5a7a6f166fb7589c12e6248561fb58.tgs"; -------------------------------------------------------------------------------- /src/assets/app-inactive.png: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "app-inactive.bc7953c2dfebcabce2c43ca7dc778a89.png"; -------------------------------------------------------------------------------- /src/assets/mastercard.svg: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "mastercard.4216118edafe23cc2dec7b8807ba4622.svg"; -------------------------------------------------------------------------------- /src/assets/monkey.svg: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "monkey.a3d5fcdc50b18dc55695f7dd4101a8c9.svg"; -------------------------------------------------------------------------------- /src/assets/telegram-logo.svg: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "telegram-logo.df3a91becaa9678c529b4c4cadd45204.svg"; -------------------------------------------------------------------------------- /src/assets/visa.svg: -------------------------------------------------------------------------------- 1 | export default __webpack_public_path__ + "visa.e5a7c336e1deb4b92a636e2e053878c4.svg"; -------------------------------------------------------------------------------- /src/bundles/main.ts: -------------------------------------------------------------------------------- 1 | import { getDispatch, getGlobal } from '../lib/teact/teactn'; 2 | 3 | import { DEBUG } from '../config'; 4 | 5 | export { default as Main } from '../components/main/Main'; 6 | 7 | if (DEBUG) { 8 | // eslint-disable-next-line no-console 9 | console.log('>>> FINISH LOAD MAIN BUNDLE'); 10 | } 11 | 12 | if (!getGlobal().connectionState) { 13 | getDispatch().initApi(); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/auth/AuthCode.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const AuthCodeAsync: FC = () => { 8 | const AuthCode = useModuleLoader(Bundles.Auth, 'AuthCode'); 9 | 10 | return AuthCode ? : ; 11 | }; 12 | 13 | export default memo(AuthCodeAsync); 14 | -------------------------------------------------------------------------------- /src/components/auth/AuthPassword.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const AuthPasswordAsync: FC = () => { 8 | const AuthPassword = useModuleLoader(Bundles.Auth, 'AuthPassword'); 9 | 10 | return AuthPassword ? : ; 11 | }; 12 | 13 | export default memo(AuthPasswordAsync); 14 | -------------------------------------------------------------------------------- /src/components/auth/AuthRegister.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const AuthRegisterAsync: FC = () => { 8 | const AuthRegister = useModuleLoader(Bundles.Auth, 'AuthRegister'); 9 | 10 | return AuthRegister ? : ; 11 | }; 12 | 13 | export default memo(AuthRegisterAsync); 14 | -------------------------------------------------------------------------------- /src/components/auth/helpers/getSuggestedLanguage.ts: -------------------------------------------------------------------------------- 1 | export function getSuggestedLanguage() { 2 | let suggestedLanguage = navigator.language; 3 | 4 | if (suggestedLanguage && suggestedLanguage !== 'pt-br') { 5 | suggestedLanguage = suggestedLanguage.substr(0, 2); 6 | } 7 | 8 | return suggestedLanguage; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/common/AnimatedEmoji.scss: -------------------------------------------------------------------------------- 1 | .AnimatedEmoji{margin-bottom:0.75rem}.AnimatedEmoji img{position:absolute;width:100%;height:100%}.AnimatedEmoji img.like-sticker-thumb{transform:scale(0.8)} 2 | -------------------------------------------------------------------------------- /src/components/common/CalendarModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './CalendarModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const CalendarModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const CalendarModal = useModuleLoader(Bundles.Extra, 'CalendarModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return CalendarModal ? : undefined; 13 | }; 14 | 15 | export default memo(CalendarModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/common/ChatLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from '../../lib/teact/teact'; 2 | import { withGlobal } from '../../lib/teact/teactn'; 3 | 4 | import { GlobalActions } from '../../global/types'; 5 | 6 | import { pick } from '../../util/iteratees'; 7 | import buildClassName from '../../util/buildClassName'; 8 | 9 | import Link from '../ui/Link'; 10 | 11 | type OwnProps = { 12 | className?: string; 13 | chatId?: number; 14 | children: any; 15 | }; 16 | 17 | type DispatchProps = Pick; 18 | 19 | const ChatLink: FC = ({ 20 | className, chatId, openChat, children, 21 | }) => { 22 | const handleClick = useCallback(() => { 23 | if (chatId) { 24 | openChat({ id: chatId }); 25 | } 26 | }, [chatId, openChat]); 27 | 28 | if (!chatId) { 29 | return children; 30 | } 31 | 32 | return ( 33 | {children} 34 | ); 35 | }; 36 | 37 | export default withGlobal( 38 | undefined, 39 | (setGlobal, actions): DispatchProps => pick(actions, ['openChat']), 40 | )(ChatLink); 41 | -------------------------------------------------------------------------------- /src/components/common/DeleteChatModal.scss: -------------------------------------------------------------------------------- 1 | .DeleteChatModal .modal-dialog{max-width:20rem}.DeleteChatModal .modal-header{padding:1.125rem 1.25rem 0}.DeleteChatModal .modal-title:not(:only-child){margin:0 0 0 .75rem}.DeleteChatModal .modal-content{padding:.5rem 1.25rem}.DeleteChatModal .confirm-dialog-button{margin-right:-.625rem} 2 | -------------------------------------------------------------------------------- /src/components/common/DeleteMessageModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './DeleteMessageModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const DeleteMessageModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const DeleteMessageModal = useModuleLoader(Bundles.Extra, 'DeleteMessageModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return DeleteMessageModal ? : undefined; 13 | }; 14 | 15 | export default memo(DeleteMessageModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/common/GifButton.scss: -------------------------------------------------------------------------------- 1 | .GifButton{display:flex;align-items:center;justify-content:center;height:6.25rem;background-color:transparent;cursor:pointer;overflow:hidden;position:relative}.GifButton:last-child{margin-bottom:1rem}.GifButton.vertical{grid-column-end:span 1}.GifButton.horizontal{grid-column-end:span 2}.GifButton .thumbnail{background-size:cover !important;background:transparent no-repeat center}.GifButton .thumbnail ~ video{position:absolute}.GifButton .thumbnail,.GifButton video{width:100%;height:100%;object-fit:cover}.GifButton:not(.shown){display:block !important;visibility:hidden}.GifButton .Spinner{position:absolute;pointer-events:none} 2 | -------------------------------------------------------------------------------- /src/components/common/LastMessageMeta.scss: -------------------------------------------------------------------------------- 1 | .LastMessageMeta{margin-right:.2rem;padding:.3rem 0 .15rem;flex-shrink:0;font-size:0.75rem;line-height:1;display:flex;align-items:center}.LastMessageMeta .MessageOutgoingStatus{color:var(--color-text-meta-colored);margin-right:0.1rem;font-size:1.15rem}body.is-ios .LastMessageMeta .MessageOutgoingStatus{margin-bottom:-.125rem}.LastMessageMeta .time{color:var(--color-text-meta);line-height:1.15rem} 2 | -------------------------------------------------------------------------------- /src/components/common/LastMessageMeta.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import { ApiMessage, ApiMessageOutgoingStatus } from '../../api/types'; 4 | 5 | import { formatPastTimeShort } from '../../util/dateFormat'; 6 | import useLang from '../../hooks/useLang'; 7 | 8 | import MessageOutgoingStatus from './MessageOutgoingStatus'; 9 | 10 | import './LastMessageMeta.scss'; 11 | 12 | type OwnProps = { 13 | message: ApiMessage; 14 | outgoingStatus?: ApiMessageOutgoingStatus; 15 | }; 16 | 17 | const LastMessageMeta: FC = ({ message, outgoingStatus }) => { 18 | const lang = useLang(); 19 | return ( 20 |
21 | {outgoingStatus && ( 22 | 23 | )} 24 | {formatPastTimeShort(lang, message.date * 1000)} 25 |
26 | ); 27 | }; 28 | 29 | export default memo(LastMessageMeta); 30 | -------------------------------------------------------------------------------- /src/components/common/Media.scss: -------------------------------------------------------------------------------- 1 | .Media{height:0;padding-bottom:100%;overflow:hidden;position:relative;cursor:pointer}.Media .video-duration{position:absolute;left:.25rem;top:.25rem;background:rgba(0,0,0,0.25);color:#fff;font-size:.75rem;padding:0 .3125rem;border-radius:.1875rem;line-height:1.125rem}.Media img{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:cover} 2 | -------------------------------------------------------------------------------- /src/components/common/MessageLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from '../../lib/teact/teact'; 2 | import { withGlobal } from '../../lib/teact/teactn'; 3 | 4 | import { GlobalActions } from '../../global/types'; 5 | import { ApiMessage } from '../../api/types'; 6 | 7 | import { pick } from '../../util/iteratees'; 8 | import buildClassName from '../../util/buildClassName'; 9 | 10 | import Link from '../ui/Link'; 11 | 12 | type OwnProps = { 13 | className?: string; 14 | message?: ApiMessage; 15 | children: any; 16 | }; 17 | 18 | type DispatchProps = Pick; 19 | 20 | const MessageLink: FC = ({ 21 | className, message, children, focusMessage, 22 | }) => { 23 | const handleMessageClick = useCallback((): void => { 24 | if (message) { 25 | focusMessage({ chatId: message.chatId, messageId: message.id }); 26 | } 27 | }, [focusMessage, message]); 28 | 29 | if (!message) { 30 | return children; 31 | } 32 | 33 | return ( 34 | {children} 35 | ); 36 | }; 37 | 38 | export default withGlobal( 39 | undefined, 40 | (setGlobal, actions): DispatchProps => pick(actions, ['focusMessage']), 41 | )(MessageLink); 42 | -------------------------------------------------------------------------------- /src/components/common/MessageOutgoingStatus.scss: -------------------------------------------------------------------------------- 1 | .MessageOutgoingStatus{width:1.19rem;height:1.19rem;overflow:hidden;display:inline-block;line-height:1;font-size:1.1875rem}.MessageOutgoingStatus i{background:var(--background-color)}.MessageOutgoingStatus .icon-message-succeeded{padding-left:0.13rem}.MessageOutgoingStatus .Transition{width:100%;height:100%} 2 | -------------------------------------------------------------------------------- /src/components/common/MessageOutgoingStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import { ApiMessageOutgoingStatus } from '../../api/types'; 4 | 5 | import Transition from '../ui/Transition'; 6 | 7 | import './MessageOutgoingStatus.scss'; 8 | 9 | type OwnProps = { 10 | status: ApiMessageOutgoingStatus; 11 | }; 12 | 13 | enum Keys { 14 | failed, pending, succeeded, read, 15 | } 16 | 17 | const MessageOutgoingStatus: FC = ({ status }) => { 18 | return ( 19 |
20 | 21 | {() => ( 22 | 23 | )} 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default memo(MessageOutgoingStatus); 30 | -------------------------------------------------------------------------------- /src/components/common/NothingFound.scss: -------------------------------------------------------------------------------- 1 | .NothingFound{display:flex;align-items:center;justify-content:center;color:var(--color-text-meta)}.NothingFound.with-description{flex-direction:column}.NothingFound .AnimatedSticker{margin:0 auto}.NothingFound .description{color:var(--color-text-secondary);font-size:.875rem;text-align:center;margin:1rem 0 0;unicode-bidi:plaintext} 2 | -------------------------------------------------------------------------------- /src/components/common/NothingFound.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import buildClassName from '../../util/buildClassName'; 4 | import useShowTransition from '../../hooks/useShowTransition'; 5 | import renderText from './helpers/renderText'; 6 | import useLang from '../../hooks/useLang'; 7 | 8 | import './NothingFound.scss'; 9 | 10 | interface OwnProps { 11 | text?: string; 12 | description?: string; 13 | } 14 | 15 | const DEFAULT_TEXT = 'Nothing found.'; 16 | 17 | const NothingFound: FC = ({ text = DEFAULT_TEXT, description }) => { 18 | const lang = useLang(); 19 | const { transitionClassNames } = useShowTransition(true); 20 | 21 | return ( 22 |
23 | {text} 24 | {description &&

{renderText(lang(description), ['br'])}

} 25 |
26 | ); 27 | }; 28 | 29 | export default memo(NothingFound); 30 | -------------------------------------------------------------------------------- /src/components/common/PasswordMonkey.scss: -------------------------------------------------------------------------------- 1 | #monkey{position:relative;display:block;margin-left:auto;margin-right:auto;width:7.5rem;height:7.5rem;margin-bottom:1.75rem}@media (min-width: 600px) and (min-height: 450px){#monkey{width:10rem;height:10rem;margin-bottom:2.5rem}}#monkey.big{width:10rem;height:10rem}#monkey .AnimatedSticker{position:absolute;left:0;top:0;width:100%;height:100%}#monkey .AnimatedSticker.hidden{display:none}#monkey .monkey-preview{width:100%;height:100%;background-size:100%;background:url(monkey.a3d5fcdc50b18dc55695f7dd4101a8c9.svg) center} 2 | -------------------------------------------------------------------------------- /src/components/common/Picker.scss: -------------------------------------------------------------------------------- 1 | .Picker{height:100%;display:flex;flex-direction:column;overflow:hidden}.Picker .picker-header{padding:0 1rem 0.25rem 0.75rem;border-bottom:1px solid var(--color-borders);display:flex;flex-flow:row wrap;flex-shrink:0;overflow-y:auto;max-height:20rem}.Picker .picker-header .input-group{margin-bottom:0.5rem;margin-left:0.5rem;flex-grow:1}.Picker .picker-header .form-control{height:2rem;border:none;border-radius:0;padding:0;box-shadow:none}.Picker .picker-list{flex-grow:1;overflow-y:auto;overflow-x:hidden;padding:0.5rem}@media (max-width: 600px){.Picker .picker-list{padding-left:0 !important;padding-right:0 !important}}.Picker .no-results{height:100%;margin:0;padding:1rem 1rem;display:flex;align-items:center;justify-content:center;color:var(--color-text-secondary)} 2 | -------------------------------------------------------------------------------- /src/components/common/StickerButton.scss: -------------------------------------------------------------------------------- 1 | .StickerButton{display:inline-block;width:4rem;height:4rem;margin:0.5rem;border-radius:var(--border-radius-messages-small);background:transparent no-repeat center;background-size:contain;cursor:pointer;transition:background-color .15s ease, opacity .3s ease !important;position:relative}@media (max-width: 600px){.StickerButton{margin:0.25rem}}.StickerButton.set-button{width:2.75rem !important;height:2.75rem;margin:0 0.5rem}.StickerButton.large{width:10rem;height:10rem;margin:0}.StickerButton .AnimatedSticker,.StickerButton img{position:absolute;top:0;left:0;width:100%;height:100%}.StickerButton img{object-fit:contain}.StickerButton .sticker-unfave-button{position:absolute;top:-0.5rem;right:-0.5rem;width:1.25rem;height:1.25rem;padding:0.125rem;opacity:0}.StickerButton .sticker-unfave-button i{font-size:1rem}.StickerButton:hover{background-color:var(--color-interactive-element-hover)}.StickerButton:hover .sticker-unfave-button{opacity:1} 2 | -------------------------------------------------------------------------------- /src/components/common/StickerSetModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './StickerSetModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const StickerSetModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const StickerSetModal = useModuleLoader(Bundles.Extra, 'StickerSetModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return StickerSetModal ? : undefined; 13 | }; 14 | 15 | export default memo(StickerSetModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/common/StickerSetModal.scss: -------------------------------------------------------------------------------- 1 | .StickerSetModal .modal-dialog{width:26.25rem;max-width:100%}@media (max-width: 600px){.StickerSetModal .modal-dialog{width:18.875rem}}.StickerSetModal .modal-header{padding:0.5rem 1rem}.StickerSetModal .modal-content{text-align:center;padding:0}.StickerSetModal .stickers{position:relative;width:100%;height:19rem;max-height:50vh;overflow-y:auto;padding:0 0.25rem;text-align:left}.StickerSetModal .button-wrapper{padding:0.5rem 0;border-top:1px solid var(--color-borders);box-shadow:0 0 2px var(--color-default-shadow)}.StickerSetModal .button-wrapper button{display:inline-block}.StickerSetModal .Loading{width:100%;height:22.8125rem;max-height:calc(50vh + 3.8125rem)} 2 | -------------------------------------------------------------------------------- /src/components/common/TypingStatus.scss: -------------------------------------------------------------------------------- 1 | .typing-status{display:flex;align-items:baseline}.typing-status .sender-name::after{content:'\00a0';color:var(--color-text-secondary)}.typing-status .ellipsis{display:flex;width:1rem;overflow:hidden}.typing-status .ellipsis::after{content:'...';animation:typing-animation 1s steps(4, start) infinite}html[lang=ar] .typing-status .ellipsis::after,html[lang=fa] .typing-status .ellipsis::after{animation-name:typing-animation-rtl}@keyframes typing-animation{from{transform:translateX(-1rem)}}@keyframes typing-animation-rtl{from{transform:translateX(1rem)}} 2 | -------------------------------------------------------------------------------- /src/components/common/UnpinAllMessagesModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './UnpinAllMessagesModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const UnpinAllMessagesModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const UnpinAllMessagesModal = useModuleLoader(Bundles.Extra, 'UnpinAllMessagesModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return UnpinAllMessagesModal ? : undefined; 13 | }; 14 | 15 | export default memo(UnpinAllMessagesModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/common/UnpinAllMessagesModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import useLang from '../../hooks/useLang'; 4 | 5 | import Modal from '../ui/Modal'; 6 | import Button from '../ui/Button'; 7 | 8 | export type OwnProps = { 9 | isOpen: boolean; 10 | chatId?: number; 11 | pinnedMessagesCount?: number; 12 | onClose: () => void; 13 | onUnpin: () => void; 14 | }; 15 | 16 | const UnpinAllMessagesModal: FC = ({ 17 | isOpen, 18 | pinnedMessagesCount = 0, 19 | onClose, 20 | onUnpin, 21 | }) => { 22 | const lang = useLang(); 23 | 24 | return ( 25 | 31 |

{lang('Chat.UnpinAllMessagesConfirmation', pinnedMessagesCount, 'i')}

32 | 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default memo(UnpinAllMessagesModal); 41 | -------------------------------------------------------------------------------- /src/components/common/UserLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from '../../lib/teact/teact'; 2 | import { withGlobal } from '../../lib/teact/teactn'; 3 | 4 | import { GlobalActions } from '../../global/types'; 5 | import { ApiChat, ApiUser } from '../../api/types'; 6 | 7 | import { pick } from '../../util/iteratees'; 8 | import buildClassName from '../../util/buildClassName'; 9 | 10 | import Link from '../ui/Link'; 11 | 12 | type OwnProps = { 13 | className?: string; 14 | sender?: ApiUser | ApiChat; 15 | children: any; 16 | }; 17 | 18 | type DispatchProps = Pick; 19 | 20 | const UserLink: FC = ({ 21 | className, sender, openUserInfo, children, 22 | }) => { 23 | const handleClick = useCallback(() => { 24 | if (sender) { 25 | openUserInfo({ id: sender.id }); 26 | } 27 | }, [sender, openUserInfo]); 28 | 29 | if (!sender) { 30 | return children; 31 | } 32 | 33 | return ( 34 | {children} 35 | ); 36 | }; 37 | 38 | export default withGlobal( 39 | undefined, 40 | (setGlobal, actions): DispatchProps => pick(actions, ['openUserInfo']), 41 | )(UserLink); 42 | -------------------------------------------------------------------------------- /src/components/common/VerifiedIcon.scss: -------------------------------------------------------------------------------- 1 | .VerifiedIcon{display:inline-block;flex-shrink:0;width:1.5rem;height:1.5rem;background-image:url(icon-verified.a2a4fb48197a45cb301b64e39d1a8427.svg);background-repeat:no-repeat;background-size:100%;background-position:center} 2 | -------------------------------------------------------------------------------- /src/components/common/VerifiedIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../lib/teact/teact'; 2 | 3 | import './VerifiedIcon.scss'; 4 | 5 | const VerifiedIcon: FC = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default VerifiedIcon; 12 | -------------------------------------------------------------------------------- /src/components/common/helpers/animatedAssets.ts: -------------------------------------------------------------------------------- 1 | import { ApiMediaFormat } from '../../../api/types'; 2 | 3 | import * as mediaLoader from '../../../util/mediaLoader'; 4 | 5 | // @ts-ignore 6 | import MonkeyIdle from '../../../assets/TwoFactorSetupMonkeyIdle.tgs'; 7 | // @ts-ignore 8 | import MonkeyTracking from '../../../assets/TwoFactorSetupMonkeyTracking.tgs'; 9 | // @ts-ignore 10 | import MonkeyClose from '../../../assets/TwoFactorSetupMonkeyClose.tgs'; 11 | // @ts-ignore 12 | import MonkeyPeek from '../../../assets/TwoFactorSetupMonkeyPeek.tgs'; 13 | // @ts-ignore 14 | import FoldersAll from '../../../assets/FoldersAll.tgs'; 15 | // @ts-ignore 16 | import FoldersNew from '../../../assets/FoldersNew.tgs'; 17 | // @ts-ignore 18 | import DiscussionGroups from '../../../assets/DiscussionGroupsDucks.tgs'; 19 | 20 | export const ANIMATED_STICKERS_PATHS = { 21 | MonkeyIdle, 22 | MonkeyTracking, 23 | MonkeyClose, 24 | MonkeyPeek, 25 | FoldersAll, 26 | FoldersNew, 27 | DiscussionGroups, 28 | }; 29 | 30 | export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) { 31 | const path = ANIMATED_STICKERS_PATHS[name].replace(window.location.origin, ''); 32 | 33 | return mediaLoader.fetch(`file${path}`, ApiMediaFormat.Lottie); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/common/helpers/detectCardType.ts: -------------------------------------------------------------------------------- 1 | const VISA = /^4[0-9]{12}(?:[0-9]{1,3})?$/; 2 | const MASTERCARD1 = /^5[1-5][0-9]{11,14}$/; 3 | const MASTERCARD2 = /^2[2-7][0-9]{11,14}$/; 4 | 5 | export enum CardType { 6 | Default, 7 | Visa, 8 | Mastercard, 9 | } 10 | 11 | const cards: Record = { 12 | [CardType.Default]: '', 13 | [CardType.Visa]: 'visa', 14 | [CardType.Mastercard]: 'mastercard', 15 | }; 16 | 17 | export function detectCardType(cardNumber: string): number { 18 | cardNumber = cardNumber.replace(/\s/g, ''); 19 | if (VISA.test(cardNumber)) { 20 | return CardType.Visa; 21 | } 22 | if (MASTERCARD1.test(cardNumber) || MASTERCARD2.test(cardNumber)) { 23 | return CardType.Mastercard; 24 | } 25 | return CardType.Default; 26 | } 27 | 28 | export function detectCardTypeText(cardNumber: string): string { 29 | const cardType = detectCardType(cardNumber); 30 | return cards[cardType as number] || ''; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/common/helpers/parseEmojiOnlyString.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import twemojiRegex from '../../../lib/twemojiRegex'; 3 | 4 | const DETECT_UP_TO = 3; 5 | const MAX_LENGTH = DETECT_UP_TO * 8; // Maximum 8 per one emoji. 6 | const RE_EMOJI_ONLY = new RegExp(`^(?:${twemojiRegex.source})+$`, ''); 7 | 8 | export default (text: string): number | false => { 9 | if (text.length > MAX_LENGTH) { 10 | return false; 11 | } 12 | 13 | const isEmojiOnly = Boolean(text.match(RE_EMOJI_ONLY)); 14 | if (!isEmojiOnly) { 15 | return false; 16 | } 17 | 18 | let emojiCount = 0; 19 | while (twemojiRegex.exec(text)) { 20 | emojiCount++; 21 | 22 | if (emojiCount > DETECT_UP_TO) { 23 | twemojiRegex.lastIndex = 0; 24 | return false; 25 | } 26 | } 27 | 28 | return emojiCount; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/left/ArchivedChats.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './ArchivedChats'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | import Loading from '../ui/Loading'; 7 | 8 | const ArchivedChatsAsync: FC = (props) => { 9 | const ArchivedChats = useModuleLoader(Bundles.Extra, 'ArchivedChats'); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return ArchivedChats ? : ; 13 | }; 14 | 15 | export default memo(ArchivedChatsAsync); 16 | -------------------------------------------------------------------------------- /src/components/left/ArchivedChats.scss: -------------------------------------------------------------------------------- 1 | .ArchivedChats{height:100%;overflow:hidden}.ArchivedChats .chat-list{height:calc(100% - var(--header-height))} 2 | -------------------------------------------------------------------------------- /src/components/left/ConnectionState.scss: -------------------------------------------------------------------------------- 1 | #ConnectionState{flex:0 0 auto;display:flex;align-items:center;margin:0 0.5rem 0.5rem;padding:0.75rem;background:var(--color-yellow);border-radius:var(--border-radius-default)}#ConnectionState>.Spinner{--spinner-size: 1.75rem}#ConnectionState>.state-text{color:var(--color-text-lighter);font-weight:500;line-height:2rem;margin-inline-start:1.875rem;white-space:nowrap}@media (max-width: 950px){#ConnectionState>.state-text{margin-inline-start:1.25rem}} 2 | -------------------------------------------------------------------------------- /src/components/left/ConnectionState.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, FC } from '../../lib/teact/teact'; 2 | 3 | import { GlobalState } from '../../global/types'; 4 | 5 | import useLang from '../../hooks/useLang'; 6 | 7 | import Spinner from '../ui/Spinner'; 8 | 9 | import './ConnectionState.scss'; 10 | 11 | type StateProps = Pick; 12 | 13 | const ConnectionState: FC = () => { 14 | const lang = useLang(); 15 | 16 | return ( 17 |
18 | 19 |
{lang('WaitingForNetwork')}
20 |
21 | ); 22 | }; 23 | 24 | export default memo(ConnectionState); 25 | -------------------------------------------------------------------------------- /src/components/left/LeftColumn.scss: -------------------------------------------------------------------------------- 1 | #LeftColumn{overflow:hidden}#NewChat{height:100%}.left-header{height:var(--header-height);padding:0.375rem 1.25rem .5rem 0.8125rem;display:flex;align-items:center;flex-shrink:0;background-color:var(--color-background)}.left-header h3{margin-bottom:0;font-size:1.25rem;font-weight:500;margin-left:1.375rem}.left-header .SearchInput{margin-left:0.875rem;max-width:calc(100% - 3.625rem)}@media (max-width: 600px){.left-header .SearchInput{max-width:calc(100% - 3.375rem)}}@media (max-width: 600px){.left-header{padding:0.5rem}} 2 | -------------------------------------------------------------------------------- /src/components/left/NewChatButton.scss: -------------------------------------------------------------------------------- 1 | .NewChatButton{position:absolute;right:1rem;bottom:1rem;transform:translateY(5rem);transition:transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)}body.animation-level-0 .NewChatButton{transform:none !important;opacity:0;transition:opacity .15s}body.animation-level-0 .NewChatButton.revealed{opacity:1}.NewChatButton.revealed{transform:translateY(0)}@media (max-width: 600px){.NewChatButton.revealed{transform:translate3d(0, 0, 10px);transform-style:preserve-3d}}.NewChatButton.menu-is-open::before{content:'';display:block;position:absolute;top:-13rem;left:-11rem;right:-1rem;bottom:-1rem;z-index:-1}.is-touch-env .NewChatButton .Menu>.backdrop{position:absolute;left:-100vw;right:-100vw;top:-100vh;bottom:-100vh}.NewChatButton>.Button .icon-new-chat-filled,.NewChatButton>.Button .icon-close{position:absolute}.NewChatButton>.Button:not(.active) .icon-new-chat-filled{animation:grow-icon .4s ease-out}.NewChatButton>.Button:not(.active) .icon-close{animation:hide-icon .4s forwards ease-out}.NewChatButton>.Button.active .icon-close{animation:grow-icon .4s ease-out}.NewChatButton>.Button.active .icon-new-chat-filled{animation:hide-icon .4s forwards ease-out} 2 | -------------------------------------------------------------------------------- /src/components/left/main/Badge.scss: -------------------------------------------------------------------------------- 1 | .Badge-transition{opacity:1;transition:transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)}.Badge-transition:not(.open){transform:scale(0);opacity:0}.Badge-transition:not(.shown){display:none}.Badge-transition.closing{transition:transform .2s ease-out, opacity .2s ease-out}.Badge-wrapper{display:flex}.Badge-wrapper .Badge{margin-inline-start:.5rem}.Badge{min-width:1.5rem;height:1.5rem;background:var(--color-gray);border-radius:0.75rem;padding:0 .4375rem;color:white;font-size:0.875rem;line-height:1.5625rem;font-weight:500;text-align:center;flex-shrink:0}body.is-macos .Badge{line-height:1.5rem}body.is-ios .Badge{line-height:1.375rem;min-width:1.375rem;height:1.375rem;padding:0 .375rem}.Badge.mention,.Badge.unread:not(.muted){background:var(--color-green);color:var(--color-white)}.Badge.pinned:not(.unread){color:var(--color-pinned);background:transparent;width:1.5rem;padding:0}.Badge.pinned:not(.unread) i{font-size:1.5rem}.Badge.mention{width:1.5rem;padding:0.25rem}.Badge.mention i{font-size:1rem;vertical-align:super}body.is-ios .Badge.mention{width:1.375rem;padding:0.25rem}body.is-ios .Badge.mention i{font-size:.875rem} 2 | -------------------------------------------------------------------------------- /src/components/left/main/ContactList.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { Bundles } from '../../../util/moduleLoader'; 3 | import { OwnProps } from './ContactList'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | import Loading from '../../ui/Loading'; 7 | 8 | const ContactListAsync: FC = (props) => { 9 | const ContactList = useModuleLoader(Bundles.Extra, 'ContactList'); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return ContactList ? : ; 13 | }; 14 | 15 | export default memo(ContactListAsync); 16 | -------------------------------------------------------------------------------- /src/components/left/main/EmptyFolder.scss: -------------------------------------------------------------------------------- 1 | .EmptyFolder{width:100%;height:80%;display:flex;align-items:center;justify-content:center;flex-direction:column}@media (max-height: 480px){.EmptyFolder{height:100%}}.EmptyFolder .sticker{height:8rem;margin-bottom:1.875rem}.EmptyFolder .title{font-size:1.25rem;margin-bottom:.125rem}.EmptyFolder .description{font-size:.875rem;color:var(--color-text-secondary)}body.is-ios .EmptyFolder .description,body.is-macos .EmptyFolder .description{color:var(--color-text-secondary-apple)}.EmptyFolder .Button.pill{margin-top:.625rem;font-weight:500;padding-inline-start:.75rem;unicode-bidi:plaintext}.EmptyFolder .Button.pill i{margin-inline-end:.625rem;font-size:1.5rem} 2 | -------------------------------------------------------------------------------- /src/components/left/main/LeftMain.scss: -------------------------------------------------------------------------------- 1 | #LeftColumn-main{height:100%;position:relative;display:flex;flex-direction:column;overflow:hidden;z-index:1}#LeftColumn-main .connection-state-wrapper{position:absolute;top:3.75rem;width:100%}#LeftColumn-main>.Transition{flex:1;overflow:hidden;transition:transform 300ms ease}#LeftColumn-main>.Transition.pull-down{transform:translateY(3.75rem)}#LeftColumn-main .ChatFolders{height:100%;display:flex;flex-direction:column;overflow:hidden}#LeftColumn-main .ChatFolders .tabs-placeholder{height:2.625rem;transition:height 150ms ease}#LeftColumn-main .ChatFolders .tabs-placeholder:not(.open){height:0}#LeftColumn-main .ChatFolders .TabList{justify-content:flex-start;padding-left:.5625rem;padding-bottom:1px;border-bottom:0;z-index:1}#LeftColumn-main .ChatFolders .Tab{flex:0 0 auto;padding-left:0.625rem;padding-right:0.625rem}#LeftColumn-main .ChatFolders .Tab>span{padding-left:0.5rem;padding-right:0.5rem}#LeftColumn-main .ChatFolders>.Transition{flex:1;overflow:hidden}#LeftColumn-main .chat-list,#LeftColumn-main .RecentContacts,#LeftColumn-main .LeftSearch,#LeftColumn-main .search-content{height:100%;overflow-y:auto}#LeftColumn-main .btn-update{position:absolute;bottom:1rem;left:1rem;right:1rem;margin:0 auto} 2 | -------------------------------------------------------------------------------- /src/components/left/main/hooks/useChatAnimationType.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from '../../../../lib/teact/teact'; 2 | 3 | export enum ChatAnimationTypes { 4 | Move, 5 | Opacity, 6 | None, 7 | } 8 | 9 | export function useChatAnimationType(orderDiffById: Record) { 10 | const movesUp = useCallback((id: number) => orderDiffById[id] < 0, [orderDiffById]); 11 | const movesDown = useCallback((id: number) => orderDiffById[id] > 0, [orderDiffById]); 12 | 13 | const orderDiffIds = Object.keys(orderDiffById).map(Number); 14 | const numberOfUp = orderDiffIds.filter(movesUp).length; 15 | const numberOfDown = orderDiffIds.filter(movesDown).length; 16 | 17 | return useCallback((chatId: number): ChatAnimationTypes => { 18 | const orderDiff = orderDiffById[chatId]; 19 | 20 | if (orderDiff === 0) { 21 | return ChatAnimationTypes.None; 22 | } 23 | 24 | if ( 25 | orderDiff === Infinity 26 | || orderDiff === -Infinity 27 | || (movesUp(chatId) && numberOfUp <= numberOfDown) 28 | || (movesDown(chatId) && numberOfDown < numberOfUp) 29 | ) { 30 | return ChatAnimationTypes.Opacity; 31 | } 32 | 33 | return ChatAnimationTypes.Move; 34 | }, [movesDown, movesUp, numberOfDown, numberOfUp, orderDiffById]); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/left/newChat/NewChat.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { Bundles } from '../../../util/moduleLoader'; 3 | 4 | import { OwnProps } from './NewChat'; 5 | 6 | import useModuleLoader from '../../../hooks/useModuleLoader'; 7 | import Loading from '../../ui/Loading'; 8 | 9 | const NewChatAsync: FC = (props) => { 10 | const NewChat = useModuleLoader(Bundles.Extra, 'NewChat'); 11 | 12 | // eslint-disable-next-line react/jsx-props-no-spreading 13 | return NewChat ? : ; 14 | }; 15 | 16 | export default memo(NewChatAsync); 17 | -------------------------------------------------------------------------------- /src/components/left/newChat/NewChat.scss: -------------------------------------------------------------------------------- 1 | .NewChat{height:100%;overflow:hidden;position:relative}.NewChat-inner{height:calc(100% - var(--header-height));overflow:hidden}.NewChat-inner.step-2{padding:0 1.25rem;display:flex;flex-direction:column}.NewChat-inner.step-2 .note,.NewChat-inner.step-2 .error{font-size:0.875rem;line-height:1.25rem;margin:1.5rem 0.25rem}.NewChat-inner.step-2 .error{color:var(--color-error)}.NewChat-inner.step-2 .note{margin-top:-0.5625rem;color:var(--color-text-secondary)}.NewChat-inner.step-2 .chat-members-heading{color:var(--color-text-secondary);font-size:1rem;font-weight:500;margin:1rem 0.25rem}.NewChat-inner.step-2 .chat-members-list{margin:0 -1.25rem;padding:0 1rem 1rem;overflow-x:hidden;flex-grow:1} 2 | -------------------------------------------------------------------------------- /src/components/left/search/ChatMessage.scss: -------------------------------------------------------------------------------- 1 | .ChatMessage:first-child{margin-top:.5rem}.ChatMessage:hover .Avatar.online::after,.ChatMessage.selected .Avatar.online::after{border-color:var(--color-chat-hover)}.ChatMessage .ListItem-button{padding:.25rem .5rem}.ChatMessage .info .title{flex-grow:1}.ChatMessage .info h3{font-size:1rem;width:auto}.ChatMessage .info .subtitle{color:var(--color-text-secondary)}.ChatMessage .info .subtitle .matching-text-highlight{color:var(--color-text);background:#CAE3F7;border-radius:0.25rem;padding:0 0.125rem}.ChatMessage .info .message{flex-grow:1;color:var(--color-text-secondary);overflow:hidden;text-overflow:ellipsis}.ChatMessage .info .message .sender-name{color:var(--color-text)}.ChatMessage .info .message .sender-name::after{content:': '}.ChatMessage .info .message img{width:1.25rem;height:1.25rem;object-fit:cover;border-radius:.125rem;vertical-align:-.25rem;margin-right:.25rem}.ChatMessage .info .message .icon-play{position:relative;display:inline-block;font-size:.75rem;color:#fff;margin-inline-start:-1.25rem;margin-inline-end:0.5rem;bottom:0.0625rem}.ChatMessage .info-row{display:flex;justify-content:space-between}.ChatMessage[dir=rtl] .subtitle{text-align:right} 2 | -------------------------------------------------------------------------------- /src/components/left/search/DateSuggest.scss: -------------------------------------------------------------------------------- 1 | .DateSuggest{display:flex;height:2rem;flex-direction:row;justify-content:space-between;margin-left:.5rem;margin-bottom:.5rem}.DateSuggest .date-item{display:flex;flex:1 1 auto;min-width:8rem;margin-top:.375rem;cursor:pointer;font-size:.875rem;font-weight:500;color:var(--color-text-secondary)}.DateSuggest .date-item .icon-calendar{font-size:1.25rem;margin-right:.25rem} 2 | -------------------------------------------------------------------------------- /src/components/left/search/LeftSearch.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { Bundles } from '../../../util/moduleLoader'; 3 | import { OwnProps } from './LeftSearch'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | import Loading from '../../ui/Loading'; 7 | 8 | const LeftSearchAsync: FC = (props) => { 9 | const LeftSearch = useModuleLoader(Bundles.Extra, 'LeftSearch'); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return LeftSearch ? : ; 13 | }; 14 | 15 | export default memo(LeftSearchAsync); 16 | -------------------------------------------------------------------------------- /src/components/left/search/helpers/getSenderName.ts: -------------------------------------------------------------------------------- 1 | import { ApiChat, ApiMessage, ApiUser } from '../../../../api/types'; 2 | import { 3 | getChatTitle, 4 | getSenderTitle, 5 | isChatPrivate, 6 | isChatGroup, 7 | } from '../../../../modules/helpers'; 8 | import { LangFn } from '../../../../hooks/useLang'; 9 | 10 | export function getSenderName( 11 | lang: LangFn, message: ApiMessage, chatsById: Record, usersById: Record, 12 | ) { 13 | const { senderId } = message; 14 | if (!senderId) { 15 | return undefined; 16 | } 17 | 18 | const sender = isChatPrivate(senderId) ? usersById[senderId] : chatsById[senderId]; 19 | 20 | let senderName = getSenderTitle(lang, sender); 21 | 22 | const chat = chatsById[message.chatId]; 23 | if (chat) { 24 | if (isChatPrivate(senderId) && (sender as ApiUser).isSelf) { 25 | senderName = `${lang('FromYou')} → ${getChatTitle(lang, chat)}`; 26 | } else if (isChatGroup(chat)) { 27 | senderName += ` → ${getChatTitle(lang, chat)}`; 28 | } 29 | } 30 | 31 | return senderName; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/left/settings/Settings.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { Bundles } from '../../../util/moduleLoader'; 3 | 4 | import { OwnProps } from './Settings'; 5 | 6 | import useModuleLoader from '../../../hooks/useModuleLoader'; 7 | import Loading from '../../ui/Loading'; 8 | 9 | const SettingsAsync: FC = (props) => { 10 | const Settings = useModuleLoader(Bundles.Extra, 'Settings'); 11 | 12 | // eslint-disable-next-line react/jsx-props-no-spreading 13 | return Settings ? : ; 14 | }; 15 | 16 | export default memo(SettingsAsync); 17 | -------------------------------------------------------------------------------- /src/components/left/settings/SettingsGeneralBackground.scss: -------------------------------------------------------------------------------- 1 | .SettingsGeneralBackground .settings-wallpapers{display:grid;padding:.5rem;grid-template-columns:repeat(3, 1fr);grid-auto-rows:1fr;grid-gap:.25rem}.SettingsGeneralBackground .Loading{height:auto !important;margin-top:5rem} 2 | -------------------------------------------------------------------------------- /src/components/left/settings/SettingsStickerSet.scss: -------------------------------------------------------------------------------- 1 | .settings-item .SettingsStickerSet.ListItem{margin-bottom:.5rem}.SettingsStickerSet .StickerButton,.SettingsStickerSet .Button{width:3rem;height:3rem;margin:0 .5rem 0 0;padding:0;flex:0 0 3rem}.SettingsStickerSet img{max-width:100%;max-height:100%}.SettingsStickerSet .multiline-menu-item{display:flex;flex-direction:column;justify-content:center}.SettingsStickerSet[dir=rtl] .StickerButton,.SettingsStickerSet[dir=rtl] .Button{margin:0 0 0 .5rem} 2 | -------------------------------------------------------------------------------- /src/components/left/settings/WallpaperTile.scss: -------------------------------------------------------------------------------- 1 | .WallpaperTile{height:0;padding-bottom:100%;cursor:pointer;position:relative}.WallpaperTile .media-inner,.WallpaperTile::after{position:absolute;left:0;top:0;width:100%;height:100%}.WallpaperTile .media-inner{overflow:hidden;transform:scale(1);transition:transform .15s ease}.WallpaperTile .media-inner img,.WallpaperTile .media-inner canvas{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:cover}.WallpaperTile::after{content:"";display:block;border:2px solid var(--color-primary);opacity:0;transition:opacity .15s ease}.WallpaperTile.selected::after{opacity:1}.WallpaperTile.selected .media-inner{transform:scale(0.9)}.WallpaperTile .spinner-container{height:100%;display:flex;align-items:center;justify-content:center} 2 | -------------------------------------------------------------------------------- /src/components/left/settings/folders/SettingsFolders.scss: -------------------------------------------------------------------------------- 1 | .settings-folders-recommended-item{width:100%;display:flex;align-items:center;justify-content:space-between}.settings-folders-list-item .ChatInfo{display:flex;align-items:center}.settings-folders-list-item .ChatInfo .Avatar{margin-left:-0.25rem;margin-right:1.5rem}.settings-folders-list-item .ChatInfo .title{display:flex;justify-content:flex-start;align-items:center}.settings-folders-list-item .ChatInfo .title h3{font-size:1rem;font-weight:500;line-height:1.3125;margin:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-align:left}.settings-folders-list-item .ChatInfo .status{display:none}.settings-folders-list-item .ChatInfo[dir=rtl] .title h3{text-align:right}.settings-folders-list-item .ListItem-button i{opacity:0.9}.settings-folders-list-item.color-primary .ListItem-button{color:var(--color-primary)}.settings-folders-list-item.color-primary .ListItem-button i{opacity:1;color:inherit}.settings-folders-list-item[dir=rtl] .Avatar{margin-left:1.5rem;margin-right:-0.25rem}.settings-item .ShowMoreButton{margin:0 -1rem;width:calc(100% + 2rem);padding-left:1rem !important} 2 | -------------------------------------------------------------------------------- /src/components/left/settings/helper/privacy.ts: -------------------------------------------------------------------------------- 1 | import { ApiPrivacyKey, SettingsScreens } from '../../../../types'; 2 | 3 | export function getPrivacyKey(screen: SettingsScreens): ApiPrivacyKey | undefined { 4 | switch (screen) { 5 | case SettingsScreens.PrivacyPhoneNumber: 6 | case SettingsScreens.PrivacyPhoneNumberAllowedContacts: 7 | case SettingsScreens.PrivacyPhoneNumberDeniedContacts: 8 | return 'phoneNumber'; 9 | case SettingsScreens.PrivacyLastSeen: 10 | case SettingsScreens.PrivacyLastSeenAllowedContacts: 11 | case SettingsScreens.PrivacyLastSeenDeniedContacts: 12 | return 'lastSeen'; 13 | case SettingsScreens.PrivacyProfilePhoto: 14 | case SettingsScreens.PrivacyProfilePhotoAllowedContacts: 15 | case SettingsScreens.PrivacyProfilePhotoDeniedContacts: 16 | return 'profilePhoto'; 17 | case SettingsScreens.PrivacyForwarding: 18 | case SettingsScreens.PrivacyForwardingAllowedContacts: 19 | case SettingsScreens.PrivacyForwardingDeniedContacts: 20 | return 'forwards'; 21 | case SettingsScreens.PrivacyGroupChats: 22 | case SettingsScreens.PrivacyGroupChatsAllowedContacts: 23 | case SettingsScreens.PrivacyGroupChatsDeniedContacts: 24 | return 'chatInvite'; 25 | } 26 | 27 | return undefined; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/main/AppInactive.scss: -------------------------------------------------------------------------------- 1 | #AppInactive{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}#AppInactive .content{max-width:28rem;margin:auto;padding:1.5rem;text-align:center}#AppInactive .title{margin-top:1rem}#AppInactive .description{color:var(--color-text-secondary);font-size:0.875rem}#AppInactive img{width:100%;max-width:20rem}#AppInactive .Button{margin-top:1rem} 2 | -------------------------------------------------------------------------------- /src/components/main/AppInactive.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../lib/teact/teact'; 2 | 3 | import Button from '../ui/Button'; 4 | 5 | import appInactivePath from '../../assets/app-inactive.png'; 6 | import './AppInactive.scss'; 7 | 8 | const AppInactive: FC = () => { 9 | const handleReload = () => { 10 | window.location.reload(); 11 | }; 12 | 13 | return ( 14 |
15 |
16 | 17 |

Such error, many tabs

18 |
19 | Telegram supports only one active tab with the app. 20 |
21 | Please reload this page to continue using this tab or close it. 22 |
23 |
24 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default AppInactive; 32 | -------------------------------------------------------------------------------- /src/components/main/Dialogs.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | 6 | const DialogsAsync: FC = ({ isOpen }) => { 7 | const Dialogs = useModuleLoader(Bundles.Extra, 'Dialogs', !isOpen); 8 | 9 | // eslint-disable-next-line react/jsx-props-no-spreading 10 | return Dialogs ? : undefined; 11 | }; 12 | 13 | export default memo(DialogsAsync); 14 | -------------------------------------------------------------------------------- /src/components/main/Dialogs.scss: -------------------------------------------------------------------------------- 1 | #Dialogs{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:var(--z-modal)} 2 | -------------------------------------------------------------------------------- /src/components/main/ForwardPicker.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | import { OwnProps } from './ForwardPicker'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const ForwardPickerAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const ForwardPicker = useModuleLoader(Bundles.Extra, 'ForwardPicker', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return ForwardPicker ? : undefined; 13 | }; 14 | 15 | export default memo(ForwardPickerAsync); 16 | -------------------------------------------------------------------------------- /src/components/main/ForwardPicker.scss: -------------------------------------------------------------------------------- 1 | .ForwardPicker{z-index:var(--z-media-viewer)}.ForwardPicker .modal-dialog{height:70%;max-width:25rem}@media (max-width: 600px){.ForwardPicker .modal-dialog{height:90%}}.ForwardPicker .modal-header{display:flex;align-items:center;padding:0.25rem 0.5rem}.ForwardPicker .modal-header .Button{margin-right:0.5rem}.ForwardPicker .modal-header .input-group{margin:0}.ForwardPicker .modal-header .form-control{border:none;box-shadow:none !important;height:2.75rem;padding:0.5rem;font-size:1.25rem;line-height:1.75rem;unicode-bidi:plaintext}.ForwardPicker .modal-content{padding:0;overflow:hidden;display:flex;flex-direction:column}.ForwardPicker .modal-content .picker-list{height:100%;overflow-x:hidden;overflow-y:auto;padding:0 1rem 1rem}.ForwardPicker .no-results{height:100%;margin:0;padding:1rem 1rem;display:flex;align-items:center;justify-content:center;color:var(--color-text-secondary)}@media (max-width: 600px){.ForwardPicker .ListItem.chat-item-clickable:not(.force-rounded-corners){margin:0}.ForwardPicker .ListItem.chat-item-clickable:not(.force-rounded-corners) .ListItem-button{border-radius:0}} 2 | -------------------------------------------------------------------------------- /src/components/main/HistoryCalendar.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import { OwnProps } from './HistoryCalendar'; 5 | 6 | import useModuleLoader from '../../hooks/useModuleLoader'; 7 | 8 | const HistoryCalendarAsync: FC = (props) => { 9 | const { isOpen } = props; 10 | const HistoryCalendar = useModuleLoader(Bundles.Extra, 'HistoryCalendar', !isOpen); 11 | 12 | // eslint-disable-next-line react/jsx-props-no-spreading 13 | return HistoryCalendar ? : undefined; 14 | }; 15 | 16 | export default memo(HistoryCalendarAsync); 17 | -------------------------------------------------------------------------------- /src/components/main/Main.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | 6 | const MainAsync: FC = () => { 7 | const Main = useModuleLoader(Bundles.Main, 'Main'); 8 | 9 | return Main ?
: undefined; 10 | }; 11 | 12 | export default memo(MainAsync); 13 | -------------------------------------------------------------------------------- /src/components/main/Notifications.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | 6 | const NotificationsAsync: FC = ({ isOpen }) => { 7 | const Notifications = useModuleLoader(Bundles.Extra, 'Notifications', !isOpen); 8 | 9 | // eslint-disable-next-line react/jsx-props-no-spreading 10 | return Notifications ? : undefined; 11 | }; 12 | 13 | export default memo(NotificationsAsync); 14 | -------------------------------------------------------------------------------- /src/components/main/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { withGlobal } from '../../lib/teact/teactn'; 3 | 4 | import { GlobalActions } from '../../global/types'; 5 | import { ApiNotification } from '../../api/types'; 6 | 7 | import { pick } from '../../util/iteratees'; 8 | 9 | import Notification from '../ui/Notification'; 10 | import renderText from '../common/helpers/renderText'; 11 | 12 | type StateProps = { 13 | notifications: ApiNotification[]; 14 | }; 15 | 16 | type DispatchProps = Pick; 17 | 18 | const Notifications: FC = ({ notifications, dismissNotification }) => { 19 | if (!notifications.length) { 20 | return undefined; 21 | } 22 | 23 | return ( 24 |
25 | {notifications.map(({ message }) => ( 26 | 30 | ))} 31 |
32 | ); 33 | }; 34 | 35 | export default memo(withGlobal( 36 | (global): StateProps => pick(global, ['notifications']), 37 | (setGlobal, actions): DispatchProps => pick(actions, ['dismissNotification']), 38 | )(Notifications)); 39 | -------------------------------------------------------------------------------- /src/components/main/SafeLinkModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import { OwnProps } from './SafeLinkModal'; 5 | 6 | import useModuleLoader from '../../hooks/useModuleLoader'; 7 | 8 | const SafeLinkModalAsync: FC = (props) => { 9 | const { url } = props; 10 | const SafeLinkModal = useModuleLoader(Bundles.Extra, 'SafeLinkModal', !url); 11 | 12 | // eslint-disable-next-line react/jsx-props-no-spreading 13 | return SafeLinkModal ? : undefined; 14 | }; 15 | 16 | export default memo(SafeLinkModalAsync); 17 | -------------------------------------------------------------------------------- /src/components/mediaViewer/MediaViewer.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import { Bundles } from '../../util/moduleLoader'; 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | 6 | interface OwnProps { 7 | isOpen: boolean; 8 | } 9 | 10 | const MediaViewerAsync: FC = ({ isOpen }) => { 11 | const MediaViewer = useModuleLoader(Bundles.Extra, 'MediaViewer', !isOpen); 12 | 13 | return MediaViewer ? : undefined; 14 | }; 15 | 16 | export default memo(MediaViewerAsync); 17 | -------------------------------------------------------------------------------- /src/components/mediaViewer/MediaViewerActions.scss: -------------------------------------------------------------------------------- 1 | .MediaViewerActions{display:flex;margin-inline-start:auto;margin-inline-end:-.375rem}.MediaViewerActions .Button{margin-inline-start:.25rem}.MediaViewerActions-mobile{position:relative}.MediaViewerActions-mobile .ProgressSpinner{position:absolute;top:0;left:0} 2 | -------------------------------------------------------------------------------- /src/components/mediaViewer/PanZoom.scss: -------------------------------------------------------------------------------- 1 | .pan-wrapper,.pan-container{position:relative;width:100%;height:100%}.pan-wrapper{cursor:move;-webkit-user-select:none;user-select:none}.pan-container{transition:transform .2s ease-in}.pan-wrapper.move .pan-container{transition:none}.zoomed .pan-container{position:fixed;top:0;left:0} 2 | -------------------------------------------------------------------------------- /src/components/mediaViewer/SenderInfo.scss: -------------------------------------------------------------------------------- 1 | .SenderInfo{display:flex;align-content:center;color:white;cursor:pointer;opacity:.5;transition:.15s opacity}.SenderInfo:hover{opacity:1}.SenderInfo .Avatar{margin-inline-end:1rem}@media (max-width: 600px){.SenderInfo .Avatar{display:none}}.SenderInfo .meta{display:flex;flex-direction:column;justify-content:center;max-width:100%}.SenderInfo .title{line-height:1.45rem;font-weight:500;white-space:pre;overflow:hidden;text-overflow:ellipsis}.SenderInfo .date{line-height:1.25rem;font-size:0.875rem;letter-spacing:-0.01rem} 2 | -------------------------------------------------------------------------------- /src/components/mediaViewer/helpers/formatFileSize.ts: -------------------------------------------------------------------------------- 1 | const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; 2 | 3 | export default (bytes: number) => { 4 | const number = bytes === 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); 5 | 6 | return `${(bytes / 1024 ** Math.floor(number)).toFixed(1)} ${units[number]}`; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/middle/ContactGreeting.scss: -------------------------------------------------------------------------------- 1 | .ContactGreeting{width:100%;height:100%;display:flex;align-items:center;justify-content:center;text-align:center}.ContactGreeting .wrapper{display:inline-flex;flex-direction:column;align-items:center;background:var(--pattern-color);width:14.5rem;padding:.75rem 1rem;border-radius:1.5rem;color:#fff}.ContactGreeting .title{font-weight:500;margin-bottom:0}.ContactGreeting .description{font-size:.9375rem;margin-bottom:0}.ContactGreeting .sticker{margin:2rem 0 1rem;height:10rem;width:10rem;cursor:pointer}.ContactGreeting .sticker .thumbnail{height:10rem;width:10rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/HeaderMenuContainer.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './HeaderMenuContainer'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const HeaderMenuContainerAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const HeaderMenuContainer = useModuleLoader(Bundles.Extra, 'HeaderMenuContainer', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return HeaderMenuContainer ? : undefined; 13 | }; 14 | 15 | export default memo(HeaderMenuContainerAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/HeaderMenuContainer.scss: -------------------------------------------------------------------------------- 1 | .HeaderMenuContainer{position:fixed;top:0;left:0;right:0;height:100vh;z-index:var(--z-header-menu)}.HeaderMenuContainer .Menu{position:absolute;font-size:1rem}.HeaderMenuContainer .Menu .backdrop{z-index:var(--z-header-menu-backdrop)}.HeaderMenuContainer .Menu .bubble{z-index:var(--z-header-menu);--offset-y: calc(100% + 1rem)} 2 | -------------------------------------------------------------------------------- /src/components/middle/MessageSelectToolbar.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | import { OwnProps } from './MessageSelectToolbar'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const MessageSelectToolbarAsync: FC = (props) => { 8 | const { isActive } = props; 9 | const MessageSelectToolbar = useModuleLoader(Bundles.Extra, 'MessageSelectToolbar', !isActive); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return MessageSelectToolbar ? : undefined; 13 | }; 14 | 15 | export default memo(MessageSelectToolbarAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/MobileSearch.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './MobileSearch'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const MobileSearchAsync: FC = (props) => { 8 | const { isActive } = props; 9 | const MobileSearch = useModuleLoader(Bundles.Extra, 'MobileSearch', !isActive, true); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return MobileSearch ? : undefined; 13 | }; 14 | 15 | export default memo(MobileSearchAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/MobileSearch.scss: -------------------------------------------------------------------------------- 1 | #MobileSearch>.header{position:absolute;top:0;left:0;z-index:var(--z-mobile-search);width:100%;height:3.5rem;background:var(--color-background);display:flex;align-items:center;padding:0 0.5rem 0 0.25rem}#MobileSearch>.header>.SearchInput{margin-left:0.25rem;flex:1}#MobileSearch>.footer{position:absolute;bottom:0;left:0;z-index:var(--z-mobile-search);width:100%;height:3.5rem;background:var(--color-background);display:flex;align-items:center;padding-left:1rem;padding-right:0.5rem}#MobileSearch>.footer>.counter{flex:1;color:var(--color-text-secondary)}#MobileSearch:not(.active) .header,#MobileSearch:not(.active) .footer{transform:translateX(-999rem)} 2 | -------------------------------------------------------------------------------- /src/components/middle/NoMessages.scss: -------------------------------------------------------------------------------- 1 | .NoMessages{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.NoMessages .icon{font-size:5rem;margin:0 auto 1rem}.NoMessages .wrapper{display:inline-flex;flex-direction:column;background:var(--pattern-color);max-width:20rem;padding:.75rem 1rem;border-radius:1.5rem;color:#fff}.NoMessages .wrapper[dir=rtl]{text-align:right}.NoMessages .title{font-weight:500;font-size:1rem;margin-bottom:.25rem;text-align:center;unicode-bidi:plaintext}.NoMessages .description{font-size:.9375rem;margin:0;padding:0;list-style:none;unicode-bidi:plaintext}.NoMessages .list-checkmarks{font-size:.9375rem;margin:.25rem 0 0;padding:0;list-style:none;unicode-bidi:plaintext;line-height:1.8}.NoMessages .list-checkmarks li::before{content:'✓';margin-inline-end:.5rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/AttachMenu.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './AttachMenu'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const AttachMenuAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const AttachMenu = useModuleLoader(Bundles.Extra, 'AttachMenu', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return AttachMenu ? : undefined; 13 | }; 14 | 15 | export default memo(AttachMenuAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/AttachMenu.scss: -------------------------------------------------------------------------------- 1 | .AttachMenu{position:relative}.is-pointer-env .AttachMenu>.backdrop{position:absolute;top:-1rem;left:auto;right:0;width:3.5rem}.AttachMenu .media-disabled>button{white-space:normal} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/AttachmentModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './AttachmentModal'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const AttachmentModalAsync: FC = (props) => { 8 | const { attachments } = props; 9 | const AttachmentModal = useModuleLoader(Bundles.Extra, 'AttachmentModal', !attachments.length); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return AttachmentModal ? : undefined; 13 | }; 14 | 15 | export default memo(AttachmentModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/BotKeyboardMenu.scss: -------------------------------------------------------------------------------- 1 | .BotKeyboardMenu .bubble{width:100% !important;max-width:27rem;border-radius:var(--border-radius-default-small)}.BotKeyboardMenu .content{display:flex;flex-direction:column;padding:.1875rem .625rem;max-height:80vh;overflow:auto}@media (max-width: 600px){.BotKeyboardMenu .content{max-height:75vh}}.BotKeyboardMenu .content .row{display:flex;flex-direction:row}.BotKeyboardMenu .content .row+.row{margin-top:.375rem}.BotKeyboardMenu .content .Button{flex:1;width:auto;height:auto;min-height:3.0625rem;border-radius:var(--border-radius-messages-small);border:2px solid var(--color-primary);background:var(--color-background);color:var(--color-primary);font-weight:500;text-transform:none}.BotKeyboardMenu .content .Button:hover{color:#fff;border-color:var(--color-primary-shade);background:var(--color-primary-shade)}.BotKeyboardMenu .content .Button+.Button{margin-left:.375rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/ComposerEmbeddedMessage.scss: -------------------------------------------------------------------------------- 1 | .ComposerEmbeddedMessage{height:2.625rem;transition:height 150ms ease-out, opacity 150ms ease-out}.select-mode-active+.middle-column-footer .ComposerEmbeddedMessage{display:none}.ComposerEmbeddedMessage:not(.open){height:0 !important}body.animation-level-0 .ComposerEmbeddedMessage{transition:none !important}.ComposerEmbeddedMessage>div{display:flex;align-items:center;padding-right:0.625rem;padding-top:0.45rem}.ComposerEmbeddedMessage>div>.Button{flex-shrink:0;background:none !important;width:2.125rem;height:2.125rem;margin:0 0.625rem;padding:0;align-self:center} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/CustomSendMenu.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './CustomSendMenu'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const CustomSendMenuAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const CustomSend = useModuleLoader(Bundles.Extra, 'CustomSendMenu', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return CustomSend ? : undefined; 13 | }; 14 | 15 | export default memo(CustomSendMenuAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/CustomSendMenu.scss: -------------------------------------------------------------------------------- 1 | .CustomSendMenu{position:relative;bottom:3.5rem}.is-pointer-env .CustomSendMenu>.backdrop{position:absolute;top:-1rem;left:auto;right:0;width:3.5rem}.CustomSendMenu .media-disabled>button{white-space:normal}.CustomSendMenu .bubble{width:16rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/DropArea.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './DropArea'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const DropAreaAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const DropArea = useModuleLoader(Bundles.Extra, 'DropArea', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return DropArea ? : undefined; 13 | }; 14 | 15 | export default memo(DropAreaAsync); 16 | export { DropAreaState } from './DropArea'; 17 | -------------------------------------------------------------------------------- /src/components/middle/composer/DropArea.scss: -------------------------------------------------------------------------------- 1 | .DropArea{position:absolute;top:0;right:0;left:0;height:100vh;z-index:var(--z-drop-area);padding:80px 20px 20px;display:flex;flex-direction:column}#Main.right-column-open .DropArea{max-width:calc(100% - var(--right-column-width))} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/EmojiButton.scss: -------------------------------------------------------------------------------- 1 | .EmojiButton{display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;margin:0.125rem;border-radius:var(--border-radius-messages-small);cursor:pointer;font-size:1.75rem;line-height:2.5rem;background-color:transparent;transition:background-color .15s ease}.mac-os-fix .EmojiButton{line-height:inherit}.EmojiButton.focus,.EmojiButton:hover{background-color:var(--color-background-selected)}.EmojiButton>img{width:2rem;height:2rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/EmojiButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, useCallback } from '../../../lib/teact/teact'; 2 | 3 | import { IS_EMOJI_SUPPORTED } from '../../../util/environment'; 4 | 5 | import './EmojiButton.scss'; 6 | 7 | type OwnProps = { 8 | emoji: Emoji; 9 | focus?: boolean; 10 | onClick: (emoji: string, name: string) => void; 11 | }; 12 | 13 | const EmojiButton: FC = ({ emoji, focus, onClick }) => { 14 | const handleClick = useCallback((e: React.MouseEvent) => { 15 | // Preventing safari from losing focus on Composer MessageInput 16 | e.preventDefault(); 17 | 18 | onClick(emoji.native, emoji.id); 19 | }, [emoji, onClick]); 20 | 21 | return ( 22 |
27 | {IS_EMOJI_SUPPORTED ? emoji.native : } 28 |
29 | ); 30 | }; 31 | 32 | export default memo(EmojiButton); 33 | -------------------------------------------------------------------------------- /src/components/middle/composer/EmojiPicker.scss: -------------------------------------------------------------------------------- 1 | .EmojiPicker{height:100%}.EmojiPicker-main{height:calc(100% - 3rem);overflow-y:auto;padding:0.5rem}@media (max-width: 600px){.EmojiPicker-main{padding:0.5rem 0.25rem}}.EmojiPicker-main .symbol-set-container{display:flex;flex-wrap:wrap}.EmojiPicker-header{height:3rem;border-bottom:1px solid var(--color-borders);display:flex;align-items:center;justify-content:space-around;box-shadow:0 0 2px var(--color-default-shadow)}@media (max-width: 600px){.EmojiPicker-header{overflow-x:auto;overflow-y:hidden;display:block;white-space:nowrap;padding:0.4375rem 0;scrollbar-width:none;scrollbar-color:rgba(0,0,0,0)}.EmojiPicker-header::-webkit-scrollbar{height:0}.EmojiPicker-header::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,0)}.EmojiPicker-header .symbol-set-button{display:inline-flex;vertical-align:middle}.EmojiPicker-header::after{content:"";display:block;flex-shrink:0;width:0.1px;height:1rem}} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/EmojiTooltip.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './EmojiTooltip'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const EmojiTooltipAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const EmojiTooltip = useModuleLoader(Bundles.Extra, 'EmojiTooltip', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return EmojiTooltip ? : undefined; 13 | }; 14 | 15 | export default memo(EmojiTooltipAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/EmojiTooltip.scss: -------------------------------------------------------------------------------- 1 | .EmojiTooltip{display:flex;padding-left:.25rem;overflow-x:auto;overflow-x:overlay;overflow-y:hidden}.EmojiTooltip .EmojiButton{flex:0 0 2.5rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/GifPicker.scss: -------------------------------------------------------------------------------- 1 | .GifPicker{display:grid;grid-template-columns:repeat(6, 1fr);grid-auto-rows:6.25rem;grid-gap:0.25rem;grid-auto-flow:dense;height:100%;overflow-y:auto;padding:0.25rem}.GifPicker .Loading,.GifPicker .picker-disabled{grid-column:1 / -1;height:var(--menu-height)} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/InlineBotTooltip.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './InlineBotTooltip'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const InlineBotTooltipAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const InlineBotTooltip = useModuleLoader(Bundles.Extra, 'InlineBotTooltip', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return InlineBotTooltip ? : undefined; 13 | }; 14 | 15 | export default memo(InlineBotTooltipAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/InlineBotTooltip.scss: -------------------------------------------------------------------------------- 1 | .InlineBotTooltip{--border-radius-default: 0}.InlineBotTooltip .switch-pm .title{margin:0 auto;font-weight:500}.InlineBotTooltip.gallery{display:grid;grid-template-columns:repeat(4, 1fr);grid-gap:1px;padding:0}@media (max-width: 600px){.InlineBotTooltip.gallery{grid-template-columns:repeat(3, 1fr)}}.InlineBotTooltip.gallery .switch-pm{grid-column:1 / -1}.InlineBotTooltip.gallery .switch-pm .ListItem-button{border-bottom-left-radius:0;border-bottom-right-radius:0}.InlineBotTooltip.gallery .GifButton{grid-column-end:initial}.InlineBotTooltip.gallery .StickerButton{width:initial;height:0;margin:0;padding-bottom:100%;border-radius:0}.InlineBotTooltip.gallery .StickerButton .AnimatedSticker,.InlineBotTooltip.gallery .StickerButton img,.InlineBotTooltip.gallery .StickerButton canvas{position:absolute;top:0;left:0;width:100% !important;height:100% !important}@media (min-width: 600px){.InlineBotTooltip.gallery .StickerButton .AnimatedSticker,.InlineBotTooltip.gallery .StickerButton img,.InlineBotTooltip.gallery .StickerButton canvas{top:.25rem;left:.25rem;width:calc(100% - .5rem) !important;height:calc(100% - .5rem) !important}} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/MentionTooltip.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './MentionTooltip'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const MentionTooltipAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const MentionTooltip = useModuleLoader(Bundles.Extra, 'MentionTooltip', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return MentionTooltip ? : undefined; 13 | }; 14 | 15 | export default memo(MentionTooltipAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/MentionTooltip.scss: -------------------------------------------------------------------------------- 1 | .MentionTooltip{width:calc(100% - 4rem);max-width:20rem;flex-direction:column;z-index:-1}@media (max-width: 600px){.MentionTooltip{width:calc(100% - 3rem)}}.MentionTooltip .ListItem.chat-item-clickable{margin:0}.MentionTooltip .ListItem.chat-item-clickable .ListItem-button{border-radius:0}.MentionTooltip .ListItem.chat-item-clickable .info{display:flex}.MentionTooltip .ListItem.chat-item-clickable .title{margin-inline-end:.625rem;max-width:70%}.MentionTooltip .ListItem.chat-item-clickable .handle{font-size:1rem}.MentionTooltip .ListItem.chat-item-clickable[dir=rtl] .status{width:auto}.MentionTooltip .ChatInfo .title h3{line-height:1.25}.MentionTooltip .ChatInfo .Avatar{margin-right:0.7em}.MentionTooltip .ChatInfo .handle::before{content:'@'}.MentionTooltip .ChatInfo .user-status{display:none !important} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/PollModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './PollModal'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const PollModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const PollModal = useModuleLoader(Bundles.Extra, 'PollModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return PollModal ? : undefined; 13 | }; 14 | 15 | export default memo(PollModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/PollModal.scss: -------------------------------------------------------------------------------- 1 | .PollModal .modal-dialog{max-width:26.25rem;max-height:calc(100vh - 5rem)}.PollModal .modal-content{padding:.5rem 1.25rem 1.875rem;min-height:4.875rem}.PollModal .modal-header-condensed{margin-bottom:1rem}.PollModal .options-header{color:var(--color-text-secondary);font-size:1rem;font-weight:500;margin:1.5rem 0.25rem}.PollModal .options-list{margin:1rem -0.75rem -0.5rem;padding:0 0.75rem;border-top:1px solid var(--color-chat-hover);max-height:20rem;overflow:auto}.PollModal .options-list.overflown{padding:0 0.4rem 0.5rem 0.75rem}@media (max-width: 600px){.PollModal .options-list{overflow:hidden;max-height:none}.PollModal .options-list,.PollModal .options-list.overflown{padding:0 0.75rem}}.PollModal .option-wrapper{position:relative}.PollModal .option-wrapper .form-control{padding-right:3rem}.PollModal .option-wrapper .option-remove-button{position:absolute;top:0.3125rem;right:0.3125rem}.PollModal .quiz-mode{margin-top:1.5rem}.PollModal .quiz-mode .options-header{margin-bottom:0.5rem}.PollModal .quiz-mode .note{margin-top:0.5rem}.PollModal .note{font-size:smaller;color:var(--color-text-secondary)}.PollModal .error{font-size:smaller;color:var(--color-error);margin:-1rem 0 1rem .25rem}.PollModal .input-group:last-child{margin-bottom:0.5rem} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/StickerTooltip.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './StickerTooltip'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const StickerTooltipAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const StickerTooltip = useModuleLoader(Bundles.Extra, 'StickerTooltip', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return StickerTooltip ? : undefined; 13 | }; 14 | 15 | export default StickerTooltipAsync; 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/StickerTooltip.scss: -------------------------------------------------------------------------------- 1 | .StickerTooltip{display:grid;grid-template-columns:repeat(auto-fill, minmax(5rem, 1fr));grid-auto-rows:auto;place-items:center}.StickerTooltip.hidden{display:none} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/SymbolMenu.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './SymbolMenu'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const SymbolMenuAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const SymbolMenu = useModuleLoader(Bundles.Extra, 'SymbolMenu', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return SymbolMenu ? : undefined; 13 | }; 14 | 15 | export default memo(SymbolMenuAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/WebPagePreview.scss: -------------------------------------------------------------------------------- 1 | .WebPagePreview{height:2.625rem;transition:height 150ms ease-out, opacity 150ms ease-out;--accent-color: var(--color-primary)}body.animation-level-0 .WebPagePreview{transition:none !important}.select-mode-active+.middle-column-footer .WebPagePreview{display:none}.WebPagePreview:not(.open){height:0 !important}.WebPagePreview>div{display:flex;align-items:center;padding-right:0.625rem;padding-top:0.45rem}.ComposerEmbeddedMessage+.WebPagePreview{margin-top:0.75rem}.WebPagePreview>div>.Button{flex-shrink:0;background:none !important;width:2.125rem;height:2.125rem;margin:0 0.625rem;padding:0;align-self:center}.WebPagePreview .WebPage{flex-grow:1;margin:0.1875rem 0 0.1875rem 0.125rem;max-width:calc(100% - 3.375rem)}.WebPagePreview .WebPage::before{top:.1rem;bottom:.05rem}.WebPagePreview .WebPage.with-video .media-inner{display:none}.WebPagePreview .WebPage .site-title,.WebPagePreview .WebPage .site-description{flex:1;max-width:100%;max-height:1rem}.WebPagePreview .WebPage .site-title{margin-top:.125rem;margin-bottom:0.1875rem}.WebPagePreview .WebPage .site-description{overflow:hidden;white-space:nowrap;text-overflow:ellipsis} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/helpers/applyIosAutoCapitalizationFix.ts: -------------------------------------------------------------------------------- 1 | import { IS_IOS } from '../../../../util/environment'; 2 | 3 | let resetInput: HTMLInputElement; 4 | 5 | if (IS_IOS) { 6 | resetInput = document.createElement('input'); 7 | resetInput.classList.add('for-ios-autocapitalization-fix'); 8 | document.body.appendChild(resetInput); 9 | } 10 | 11 | // https://stackoverflow.com/a/55652503 12 | export default function applyIosAutoCapitalizationFix(inputEl: HTMLElement) { 13 | resetInput.focus(); 14 | inputEl.focus(); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/helpers/getMessageTextAsHtml.ts: -------------------------------------------------------------------------------- 1 | import { ApiFormattedText } from '../../../../api/types'; 2 | import { renderTextWithEntities } from '../../../common/helpers/renderMessageText'; 3 | 4 | export default function getMessageTextAsHtml(formattedText?: ApiFormattedText) { 5 | const { text, entities } = formattedText || {}; 6 | if (!text) { 7 | return ''; 8 | } 9 | 10 | const result = renderTextWithEntities( 11 | text, 12 | entities, 13 | undefined, 14 | undefined, 15 | true, 16 | ); 17 | 18 | if (Array.isArray(result)) { 19 | return result.join(''); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/middle/composer/helpers/searchUserName.ts: -------------------------------------------------------------------------------- 1 | import { ApiUser } from '../../../../api/types'; 2 | import { getUserFullName } from '../../../../modules/helpers'; 3 | import searchWords from '../../../../util/searchWords'; 4 | 5 | // TODO: Support cyrillic translit search 6 | export default function searchUserName(filter: string, user: ApiUser) { 7 | const usernameLowered = user.username.toLowerCase(); 8 | const fullName = getUserFullName(user); 9 | const fullNameLowered = fullName && fullName.toLowerCase(); 10 | const filterLowered = filter.toLowerCase(); 11 | 12 | return usernameLowered.startsWith(filterLowered) || ( 13 | fullNameLowered && searchWords(fullNameLowered, filterLowered) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/middle/composer/helpers/selection.ts: -------------------------------------------------------------------------------- 1 | const MAX_NESTING_PARENTS = 5; 2 | 3 | export function isSelectionInsideInput(selectionRange: Range, inputId: string) { 4 | const { commonAncestorContainer } = selectionRange; 5 | let parentNode: HTMLElement | null = commonAncestorContainer as HTMLElement; 6 | let iterations = 1; 7 | while (parentNode && parentNode.id !== inputId && iterations < MAX_NESTING_PARENTS) { 8 | parentNode = parentNode.parentElement; 9 | iterations++; 10 | } 11 | 12 | return Boolean(parentNode && parentNode.id === inputId); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/middle/composer/inlineResults/ArticleResult.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo, useCallback } from '../../../../lib/teact/teact'; 2 | 3 | import { ApiBotInlineResult } from '../../../../api/types'; 4 | 5 | import BaseResult from './BaseResult'; 6 | 7 | export type OwnProps = { 8 | focus?: boolean; 9 | inlineResult: ApiBotInlineResult; 10 | onClick: (result: ApiBotInlineResult) => void; 11 | }; 12 | 13 | const ArticleResult: FC = ({ focus, inlineResult, onClick }) => { 14 | const { 15 | title, url, description, webThumbnail, 16 | } = inlineResult; 17 | 18 | const handleClick = useCallback(() => { 19 | onClick(inlineResult); 20 | }, [inlineResult, onClick]); 21 | 22 | return ( 23 | 30 | ); 31 | }; 32 | 33 | export default memo(ArticleResult); 34 | -------------------------------------------------------------------------------- /src/components/middle/composer/inlineResults/BaseResult.scss: -------------------------------------------------------------------------------- 1 | .BaseResult.chat-item-clickable>.ListItem-button{padding-left:1.25rem !important;padding-right:1.25rem !important}.BaseResult.chat-item-clickable>.ListItem-button .title{display:block;text-overflow:ellipsis}.BaseResult .thumb{background-color:var(--color-background-secondary);flex:0 0 3rem;width:3rem;height:3rem;line-height:3rem;border-radius:var(--border-radius-default-tiny);display:inline-flex;align-content:center;justify-content:center;margin-inline-end:.5rem;overflow:hidden;font-size:1.5rem}.BaseResult .thumb img:not(.emoji){width:100%;object-fit:cover}.BaseResult .thumb img.emoji{width:1.5rem;height:1.5rem;margin:.75rem 0 0}.BaseResult .content-inner{min-width:0}.BaseResult .title{font-weight:500;text-align:left;unicode-bidi:plaintext}.BaseResult .description{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;text-align:left;unicode-bidi:plaintext}.BaseResult[dir=rtl] .title,.BaseResult[dir=rtl] .description{text-align:right} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/inlineResults/GifResult.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, memo, useCallback, 3 | } from '../../../../lib/teact/teact'; 4 | 5 | import { ApiBotInlineMediaResult, ApiBotInlineResult } from '../../../../api/types'; 6 | 7 | import { ObserveFn } from '../../../../hooks/useIntersectionObserver'; 8 | 9 | import GifButton from '../../../common/GifButton'; 10 | 11 | type OwnProps = { 12 | inlineResult: ApiBotInlineMediaResult; 13 | observeIntersection: ObserveFn; 14 | onClick: (result: ApiBotInlineResult) => void; 15 | }; 16 | 17 | const GifResult: FC = ({ 18 | inlineResult, observeIntersection, onClick, 19 | }) => { 20 | const { gif } = inlineResult; 21 | 22 | const handleClick = useCallback(() => { 23 | onClick(inlineResult); 24 | }, [inlineResult, onClick]); 25 | 26 | if (!gif) { 27 | return undefined; 28 | } 29 | 30 | return ( 31 | 37 | ); 38 | }; 39 | 40 | export default memo(GifResult); 41 | -------------------------------------------------------------------------------- /src/components/middle/composer/inlineResults/MediaResult.scss: -------------------------------------------------------------------------------- 1 | .MediaResult{height:0;padding-bottom:100%;overflow:hidden;position:relative;cursor:pointer}.MediaResult img{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:cover} 2 | -------------------------------------------------------------------------------- /src/components/middle/composer/inlineResults/StickerResult.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../../lib/teact/teact'; 2 | 3 | import { ApiBotInlineMediaResult, ApiBotInlineResult } from '../../../../api/types'; 4 | 5 | import { STICKER_SIZE_INLINE_BOT_RESULT } from '../../../../config'; 6 | import { ObserveFn } from '../../../../hooks/useIntersectionObserver'; 7 | 8 | import StickerButton from '../../../common/StickerButton'; 9 | 10 | type OwnProps = { 11 | inlineResult: ApiBotInlineMediaResult; 12 | observeIntersection: ObserveFn; 13 | onClick: (result: ApiBotInlineResult) => void; 14 | }; 15 | 16 | const StickerResult: FC = ({ inlineResult, observeIntersection, onClick }) => { 17 | const { sticker } = inlineResult; 18 | 19 | if (!sticker) { 20 | return undefined; 21 | } 22 | 23 | return ( 24 | 33 | ); 34 | }; 35 | 36 | export default memo(StickerResult); 37 | -------------------------------------------------------------------------------- /src/components/middle/helpers/getCurrencySign.ts: -------------------------------------------------------------------------------- 1 | const CURRENCIES: Record = { 2 | USD: '$', 3 | EUR: '€', 4 | GBP: '£', 5 | JPY: '¥', 6 | RUB: '₽', 7 | UAH: '₴', 8 | INR: '₹', 9 | AED: 'د.إ', 10 | }; 11 | 12 | export function getCurrencySign(currency: string | undefined): string { 13 | if (!currency) { 14 | return ''; 15 | } 16 | return CURRENCIES[currency] || ''; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/middle/helpers/inputFormatters.ts: -------------------------------------------------------------------------------- 1 | export function formatCardExpiry(input: string) { 2 | input = input.replace(/[^\d]/g, '').slice(0, 4); 3 | const parts = input.match(/.{1,2}/g); 4 | if (parts && parts[0] && Number(parts[0]) > 12) { 5 | parts[0] = '12'; 6 | } 7 | if (parts && parts[0] && parts[0].length === 2 && !parts[1]) { 8 | parts[1] = ''; 9 | } 10 | return parts ? parts.join('/') : ''; 11 | } 12 | 13 | export function formatCardNumber(input: string) { 14 | input = input.replace(/[^\d]/g, ''); 15 | const parts = input.match(/.{1,4}/g); 16 | return parts ? parts.join(' ') : ''; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/middle/helpers/preventMessageInputBlur.ts: -------------------------------------------------------------------------------- 1 | import React from '../../../lib/teact/teact'; 2 | 3 | import { EDITABLE_INPUT_ID } from '../../../config'; 4 | import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; 5 | 6 | export function preventMessageInputBlur(e: React.MouseEvent) { 7 | if ( 8 | IS_SINGLE_COLUMN_LAYOUT 9 | || !document.activeElement 10 | || document.activeElement.id !== EDITABLE_INPUT_ID 11 | || e.target !== e.currentTarget 12 | ) { 13 | return; 14 | } 15 | 16 | e.preventDefault(); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/middle/message/Album.scss: -------------------------------------------------------------------------------- 1 | .Album{position:relative;overflow:hidden}.message-content.media.text .Album{margin:-0.3125rem -0.5rem 0.3125rem}.forwarded-message .Album{margin-bottom:.125rem}.message-content.media.text .forwarded-message .Album{margin:0 0 0.3125rem;--border-bottom-left-radius: inherit;--border-bottom-right-radius: inherit}.Album>.album-item-select-wrapper .media-inner,.message-content.media.text .Album>.album-item-select-wrapper .media-inner{margin:0 !important}.Album>.album-item-select-wrapper .media-inner,.Album>.album-item-select-wrapper .media-inner img,.Album>.album-item-select-wrapper .media-inner video{border-radius:0 !important;object-fit:cover} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/Contact.scss: -------------------------------------------------------------------------------- 1 | .Contact{display:flex;align-items:center;padding:0.25rem}.Contact.interactive{cursor:pointer}.Contact .Avatar{margin-right:0.8rem}.Contact .contact-info{padding:0.5rem;padding-left:0;white-space:nowrap;overflow:hidden}.Contact .contact-info .contact-name{font-size:1rem;line-height:1rem;margin-bottom:0.25rem;font-weight:500}.Contact .contact-info .contact-phone{line-height:1rem;color:var(--secondary-color)} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/ContextMenuContainer.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { OwnProps } from './ContextMenuContainer'; 3 | import { Bundles } from '../../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../../hooks/useModuleLoader'; 6 | 7 | const ContextMenuContainerAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const ContextMenuContainer = useModuleLoader(Bundles.Extra, 'ContextMenuContainer', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return ContextMenuContainer ? : undefined; 13 | }; 14 | 15 | export default memo(ContextMenuContainerAsync); 16 | -------------------------------------------------------------------------------- /src/components/middle/message/InlineButtons.scss: -------------------------------------------------------------------------------- 1 | .InlineButtons{display:flex;flex-direction:column}.InlineButtons .row{display:flex;flex-direction:row}.InlineButtons .Button{flex:1;width:auto;margin:0.125rem;background:var(--pattern-color);border-radius:var(--border-radius-messages-small);font-weight:500;text-transform:none}.InlineButtons .Button::before{content:'';background-color:var(--color-white);opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;border-radius:var(--border-radius-messages-small);z-index:var(--z-below);transition:opacity 200ms}.InlineButtons .Button:hover{background:var(--pattern-color) !important}.InlineButtons .Button:hover::before{opacity:.4}.InlineButtons .Button:first-of-type{margin-left:0}.InlineButtons .Button:last-of-type{margin-right:0}.InlineButtons .Button i{font-size:0.75rem;position:absolute;right:0.125rem;top:0.125rem;display:block;transform:rotate(-45deg)}.InlineButtons .row:first-of-type .Button{margin-top:0.25rem !important}.InlineButtons .row:last-of-type .Button{margin-bottom:0}.InlineButtons .row:last-of-type .Button:first-of-type{border-bottom-left-radius:var(--border-radius-messages)}.InlineButtons .row:last-of-type .Button:last-of-type{border-bottom-right-radius:var(--border-radius-messages)} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/InlineButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../../lib/teact/teact'; 2 | 3 | import { ApiKeyboardButton, ApiMessage } from '../../../api/types'; 4 | 5 | import { RE_TME_LINK } from '../../../config'; 6 | import renderText from '../../common/helpers/renderText'; 7 | 8 | import Button from '../../ui/Button'; 9 | 10 | import './InlineButtons.scss'; 11 | 12 | type OwnProps = { 13 | message: ApiMessage; 14 | onClick: ({ button }: { button: ApiKeyboardButton }) => void; 15 | }; 16 | 17 | const InlineButtons: FC = ({ message, onClick }) => { 18 | return ( 19 |
20 | {message.inlineButtons!.map((row) => ( 21 |
22 | {row.map((button) => ( 23 | 32 | ))} 33 |
34 | ))} 35 |
36 | ); 37 | }; 38 | 39 | export default InlineButtons; 40 | -------------------------------------------------------------------------------- /src/components/middle/message/Invoice.scss: -------------------------------------------------------------------------------- 1 | .Invoice .title{color:var(--accent-color);font-weight:500}.Invoice .description{position:relative}.Invoice .description.has-image .invoice-image{max-width:100%;height:20rem}@media (max-width: 600px){.Invoice .description.has-image .invoice-image{height:10rem}}.Invoice .description.has-image .description-text{position:absolute;top:0;padding:.25rem .5rem;margin:.25rem;background-color:rgba(90,110,70,0.6);border-radius:var(--border-radius-messages-small);color:var(--color-text);font-weight:500} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/MessageContextMenu.scss: -------------------------------------------------------------------------------- 1 | .MessageContextMenu{position:absolute;font-size:1rem}.MessageContextMenu .bubble{transform:scale(0.5);transition:opacity 0.15s cubic-bezier(0.2, 0, 0.2, 1),transform 0.15s cubic-bezier(0.2, 0, 0.2, 1) !important}.MessageContextMenu .backdrop{position:absolute;touch-action:none} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/RoundVideo.scss: -------------------------------------------------------------------------------- 1 | .RoundVideo{position:relative;width:200px;height:200px;cursor:pointer}.RoundVideo .thumbnail-wrapper{width:200px;height:200px;border-radius:50%;overflow:hidden}.RoundVideo .video-wrapper{position:absolute;left:0;top:0;border-radius:50%;overflow:hidden}.RoundVideo .progress{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.RoundVideo .progress-circle{stroke:white;fill:transparent;stroke-width:4;stroke-opacity:.35;stroke-linecap:round}.RoundVideo video::-internal-media-controls-cast-button,.RoundVideo video::-webkit-media-controls,.RoundVideo video::-webkit-media-controls-start-playback-button{display:none} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/Sticker.scss: -------------------------------------------------------------------------------- 1 | .Sticker:not(.inactive){cursor:pointer}.Sticker.inactive{pointer-events:none} 2 | -------------------------------------------------------------------------------- /src/components/middle/message/helpers/calculateAuthorWidth.ts: -------------------------------------------------------------------------------- 1 | import { IS_IOS } from '../../../../util/environment'; 2 | 3 | let element: HTMLSpanElement | undefined; 4 | 5 | export default function calculateAuthorWidth(text: string) { 6 | if (!element) { 7 | element = document.createElement('span'); 8 | // eslint-disable-next-line max-len 9 | element.style.font = IS_IOS 10 | // eslint-disable-next-line max-len 11 | ? '400 12px system-ui, -apple-system, BlinkMacSystemFont, "Roboto", "Apple Color Emoji", "Helvetica Neue", sans-serif' 12 | : '400 12px "Roboto", -apple-system, "Apple Color Emoji", BlinkMacSystemFont, "Helvetica Neue", sans-serif'; 13 | element.style.whiteSpace = 'nowrap'; 14 | element.style.position = 'absolute'; 15 | element.style.left = '-999px'; 16 | element.style.opacity = '.01'; 17 | document.body.appendChild(element); 18 | } 19 | 20 | element.innerHTML = text; 21 | 22 | return element.offsetWidth; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/middle/message/helpers/getCustomAppendixBg.ts: -------------------------------------------------------------------------------- 1 | const SELECTED_APPENDIX_BACKGROUND = Promise.resolve('rgba(255,255,255,1)'); 2 | 3 | export default function getCustomAppendixBg(src: string, isOwn: boolean, inSelectMode?: boolean, isSelected?: boolean) { 4 | return isSelected ? SELECTED_APPENDIX_BACKGROUND : getAppendixColorFromImage(src, isOwn); 5 | } 6 | 7 | async function getAppendixColorFromImage(src: string, isOwn: boolean) { 8 | const img = new Image(); 9 | img.src = src; 10 | 11 | if (!img.width) { 12 | await new Promise((resolve) => { 13 | img.onload = resolve; 14 | }); 15 | } 16 | 17 | const canvas = document.createElement('canvas'); 18 | const ctx = canvas.getContext('2d')!; 19 | 20 | canvas.width = img.width; 21 | canvas.height = img.height; 22 | 23 | ctx.drawImage(img, 0, 0, img.width, img.height); 24 | 25 | const x = isOwn ? img.width - 1 : 0; 26 | const y = img.height - 1; 27 | 28 | const pixel = Array.from(ctx.getImageData(x, y, 1, 1).data); 29 | return `rgba(${pixel.join(',')})`; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/middle/message/hooks/useBlurredMediaThumbRef.ts: -------------------------------------------------------------------------------- 1 | import { ApiMessage } from '../../../../api/types'; 2 | 3 | import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment'; 4 | import { getMessageMediaThumbDataUri } from '../../../../modules/helpers'; 5 | import useCanvasBlur from '../../../../hooks/useCanvasBlur'; 6 | 7 | export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) { 8 | return useCanvasBlur( 9 | getMessageMediaThumbDataUri(message), 10 | Boolean(fullMediaData), 11 | IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED, 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/middle/message/hooks/useFocusMessage.ts: -------------------------------------------------------------------------------- 1 | import { FocusDirection } from '../../../../types'; 2 | 3 | import { useLayoutEffect } from '../../../../lib/teact/teact'; 4 | import fastSmoothScroll from '../../../../util/fastSmoothScroll'; 5 | 6 | // This is used when the viewport was replaced. 7 | const RELOCATED_FOCUS_OFFSET = 1000; 8 | const FOCUS_MARGIN = 20; 9 | 10 | export default function useFocusMessage( 11 | elementRef: { current: HTMLDivElement | null }, 12 | chatId: number, 13 | isFocused?: boolean, 14 | focusDirection?: FocusDirection, 15 | noFocusHighlight?: boolean, 16 | ) { 17 | useLayoutEffect(() => { 18 | if (isFocused && elementRef.current) { 19 | const messagesContainer = elementRef.current.closest('.MessageList')!; 20 | 21 | fastSmoothScroll( 22 | messagesContainer, 23 | elementRef.current, 24 | // `noFocusHighlight` always called from “scroll-to-bottom” buttons 25 | noFocusHighlight ? 'end' : 'centerOrTop', 26 | FOCUS_MARGIN, 27 | focusDirection !== undefined ? RELOCATED_FOCUS_OFFSET : undefined, 28 | focusDirection, 29 | ); 30 | } 31 | }, [elementRef, chatId, isFocused, focusDirection, noFocusHighlight]); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/payment/CardInput.scss: -------------------------------------------------------------------------------- 1 | .CardInput{position:relative}.CardInput .input-group.has-left-addon .form-control{padding-left:4rem}.CardInput .left-addon{position:absolute;top:.8rem;left:1rem;z-index:8}.CardInput .left-addon img{max-width:2rem} 2 | -------------------------------------------------------------------------------- /src/components/payment/PaymentInfo.scss: -------------------------------------------------------------------------------- 1 | .PaymentInfo{padding:0.5rem 1rem}.PaymentInfo h5{font-size:0.9rem;color:var(--color-text-secondary);margin:1rem 0 1.1rem}.PaymentInfo .inline-inputs{display:flex;justify-content:space-between}.PaymentInfo .inline-inputs .input-group{flex:1 10rem;max-width:45%;display:flex} 2 | -------------------------------------------------------------------------------- /src/components/payment/PaymentModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './PaymentModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const PaymentModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const PaymentModal = useModuleLoader(Bundles.Extra, 'PaymentModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return PaymentModal ? : undefined; 13 | }; 14 | 15 | export default memo(PaymentModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/payment/PaymentModal.scss: -------------------------------------------------------------------------------- 1 | .PaymentModal .modal-backdrop{pointer-events:none}.PaymentModal .header{position:relative;border-top-left-radius:10px;border-top-right-radius:10px;width:100%;padding:.25rem 1rem;display:flex;align-items:center;flex-direction:row;background:var(--color-background);border-bottom:1px var(--color-borders) solid}.PaymentModal .header h3{margin-bottom:0;margin-left:1.5rem;unicode-bidi:plaintext;text-align:initial}.PaymentModal .Transition{height:25rem}.PaymentModal .empty-content{height:25rem;max-height:90%;display:flex;align-items:center;justify-content:center}.PaymentModal .receipt-content{height:25rem;overflow-y:auto}.PaymentModal .content{overflow:auto;width:100%;height:100%;position:relative}.PaymentModal .footer{position:relative;border-bottom-left-radius:10px;border-bottom-right-radius:10px;width:100%;padding:.75rem 1rem;background:var(--color-background);border-top:1px var(--color-borders) solid}.PaymentModal .footer button{text-transform:none;font-weight:500}.PaymentModal .modal-dialog{width:25rem}.PaymentModal .modal-content{padding:0;overflow:hidden}@media screen and (max-device-width: 640px) and (max-height: 640px) and (orientation: landscape){.PaymentModal .modal-dialog{max-height:100%}.PaymentModal .Transition{height:10rem}} 2 | -------------------------------------------------------------------------------- /src/components/payment/ReceiptModal.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './ReceiptModal'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | 7 | const ReceiptModalAsync: FC = (props) => { 8 | const { isOpen } = props; 9 | const ReceiptModal = useModuleLoader(Bundles.Extra, 'ReceiptModal', !isOpen); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return ReceiptModal ? : undefined; 13 | }; 14 | 15 | export default memo(ReceiptModalAsync); 16 | -------------------------------------------------------------------------------- /src/components/payment/Shipping.scss: -------------------------------------------------------------------------------- 1 | .Shipping{padding:0.5rem 1rem}.Shipping form p{color:var(--color-text-secondary);font-weight:500;margin:.5rem 0 2rem}.Shipping form .Radio{margin-bottom:2rem} 2 | -------------------------------------------------------------------------------- /src/components/payment/ShippingInfo.scss: -------------------------------------------------------------------------------- 1 | .ShippingInfo{padding:0.5rem 1rem}.ShippingInfo h5{font-size:0.9rem;color:var(--color-text-secondary);margin:1rem 0 1.1rem} 2 | -------------------------------------------------------------------------------- /src/components/right/AddChatMembers.scss: -------------------------------------------------------------------------------- 1 | .AddChatMembers{height:100%;overflow:hidden;position:relative}.AddChatMembers-inner{height:100%;overflow:hidden} 2 | -------------------------------------------------------------------------------- /src/components/right/GifSearch.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const GifSearchAsync: FC = () => { 8 | const GifSearch = useModuleLoader(Bundles.Extra, 'GifSearch'); 9 | 10 | // eslint-disable-next-line react/jsx-props-no-spreading 11 | return GifSearch ? : ; 12 | }; 13 | 14 | export default memo(GifSearchAsync); 15 | -------------------------------------------------------------------------------- /src/components/right/GifSearch.scss: -------------------------------------------------------------------------------- 1 | .GifSearch{height:100%;padding:0.25rem}.GifSearch .gif-container{height:100%;overflow:auto}.GifSearch .gif-container.grid{display:grid;grid-template-columns:repeat(6, 1fr);grid-auto-rows:6.25rem;grid-gap:0.25rem;grid-auto-flow:dense}.GifSearch .helper-text{color:var(--color-text-meta);margin-top:2rem;text-align:center} 2 | -------------------------------------------------------------------------------- /src/components/right/PollAnswerResults.scss: -------------------------------------------------------------------------------- 1 | .PollAnswerResults{border-bottom:1px solid var(--color-borders);padding:0 .5rem .625rem;display:flex;flex-direction:column-reverse}.PollAnswerResults .answer-head{display:flex;align-items:center;font-size:.9375rem;line-height:1.3125rem;font-weight:500;color:var(--color-text-secondary);padding:1rem .75rem .5rem 1rem;position:sticky;top:0;background:var(--color-background)}@media (max-width: 600px){.PollAnswerResults .answer-head{padding:.5rem .25rem .5rem .5rem}}.PollAnswerResults .answer-percent{margin-left:auto}.PollAnswerResults .answer-percent[dir=auto]{margin-left:0;margin-right:auto}.PollAnswerResults .poll-voters{padding:0 .75rem;position:relative;min-height:3rem}@media (max-width: 600px){.PollAnswerResults .poll-voters{padding:0 .25rem}}.PollAnswerResults .poll-voters .Spinner{--spinner-size: 1.25rem}.PollAnswerResults .chat-item-clickable .ChatInfo .Avatar.size-tiny{margin-right:1.75rem}.PollAnswerResults .chat-item-clickable[dir=rtl] .ChatInfo .Avatar.size-tiny{margin-left:1.75rem;margin-right:0}.PollAnswerResults .ShowMoreButton{margin:.25rem 0 0 -0.5rem;width:calc(100% + 1rem)}.PollAnswerResults .ShowMoreButton[dir=rtl] .icon-down{margin-left:2rem;margin-right:0}.PollAnswerResults .icon-down{vertical-align:middle;margin-right:2rem;font-size:1.5rem} 2 | -------------------------------------------------------------------------------- /src/components/right/PollResults.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const PollResultsAsync: FC = () => { 8 | const PollResults = useModuleLoader(Bundles.Extra, 'PollResults'); 9 | 10 | return PollResults ? : ; 11 | }; 12 | 13 | export default memo(PollResultsAsync); 14 | -------------------------------------------------------------------------------- /src/components/right/PollResults.scss: -------------------------------------------------------------------------------- 1 | .PollResults{height:100%;position:relative;display:flex;flex-direction:column;pointer-events:auto}.PollResults>.Loading{position:absolute;top:0;left:0;bottom:0;right:0;background:rgba(255,255,255,0.75)}.PollResults .poll-question{padding:.75rem 1.5rem;flex-shrink:0;font-size:1.25rem}@media (max-width: 600px){.PollResults .poll-question{padding:0 1rem}}.PollResults .poll-results-list{border-top:1px solid var(--color-borders);flex-grow:1;overflow:auto} 2 | -------------------------------------------------------------------------------- /src/components/right/ProfilePhoto.scss: -------------------------------------------------------------------------------- 1 | .ProfilePhoto{width:100%;height:100%;cursor:pointer;position:relative}.ProfilePhoto img{width:100%;object-fit:cover}.ProfilePhoto .prev-avatar-media{position:absolute;left:0;top:0;z-index:-1}.ProfilePhoto .spinner-wrapper{width:100%;height:100%}.ProfilePhoto .spinner-wrapper,.ProfilePhoto.deleted-account,.ProfilePhoto.no-photo,.ProfilePhoto.saved-messages{display:flex;align-items:center;justify-content:center;color:var(--color-white);background:linear-gradient(var(--color-white) -125%, var(--color-user));cursor:default}.ProfilePhoto.no-photo{font-size:14rem}.ProfilePhoto.deleted-account,.ProfilePhoto.saved-messages{font-size:20rem} 2 | -------------------------------------------------------------------------------- /src/components/right/RightHeader.scss: -------------------------------------------------------------------------------- 1 | .RightHeader{display:flex;align-items:center;height:var(--header-height);padding:0.5rem .8125rem;pointer-events:auto}@media (max-width: 600px){.RightHeader{padding:0.5rem}}.RightHeader .close-button{flex-shrink:0}.RightHeader>.Transition{flex:1;height:100%}.RightHeader>.Transition>div{display:flex;align-items:center}.RightHeader h3{margin-bottom:0;font-size:1.25rem;font-weight:500;margin-left:1.375rem}.RightHeader .tools{display:flex;margin-left:auto}.RightHeader .SearchInput{margin-left:1rem}@media (min-width: 600px){.RightHeader .SearchInput{margin-right:1rem}}.RightHeader .DropdownMenu{margin-left:auto} 2 | -------------------------------------------------------------------------------- /src/components/right/RightSearch.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { OwnProps } from './RightSearch'; 3 | import { Bundles } from '../../util/moduleLoader'; 4 | 5 | import useModuleLoader from '../../hooks/useModuleLoader'; 6 | import Loading from '../ui/Loading'; 7 | 8 | const RightSearchAsync: FC = (props) => { 9 | const RightSearch = useModuleLoader(Bundles.Extra, 'RightSearch'); 10 | 11 | // eslint-disable-next-line react/jsx-props-no-spreading 12 | return RightSearch ? : ; 13 | }; 14 | 15 | export default memo(RightSearchAsync); 16 | -------------------------------------------------------------------------------- /src/components/right/RightSearch.scss: -------------------------------------------------------------------------------- 1 | .RightSearch{height:100%;padding:0 0.5rem;overflow-y:auto}.RightSearch .helper-text{padding:1rem;margin-bottom:0.125rem;font-weight:500;color:var(--color-text-secondary);unicode-bidi:plaintext;text-align:initial} 2 | -------------------------------------------------------------------------------- /src/components/right/StickerSearch.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | import { Bundles } from '../../util/moduleLoader'; 3 | 4 | import useModuleLoader from '../../hooks/useModuleLoader'; 5 | import Loading from '../ui/Loading'; 6 | 7 | const StickerSearchAsync: FC = () => { 8 | const StickerSearch = useModuleLoader(Bundles.Extra, 'StickerSearch'); 9 | 10 | // eslint-disable-next-line react/jsx-props-no-spreading 11 | return StickerSearch ? : ; 12 | }; 13 | 14 | export default memo(StickerSearchAsync); 15 | -------------------------------------------------------------------------------- /src/components/right/StickerSearch.scss: -------------------------------------------------------------------------------- 1 | .StickerSearch{height:100%;padding:0 0.5rem;overflow-y:auto}.StickerSearch .helper-text{padding:1rem;margin-bottom:0.125rem;font-weight:500;color:var(--color-text-secondary)}.StickerSearch .sticker-set{margin-bottom:1rem}.StickerSearch .sticker-set-header{display:flex;justify-content:space-between;padding:0.5rem}.StickerSearch .sticker-set-header .title-wrapper{overflow:hidden}.StickerSearch .sticker-set-header .title{font-size:1rem;line-height:1.6875rem;margin:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.StickerSearch .sticker-set-header .count{color:var(--color-text-secondary);margin:0}.StickerSearch .sticker-set-header .Button{flex-shrink:0;margin-left:1rem}.StickerSearch .sticker-set-header .Button.is-added{background:var(--color-chat-hover);color:var(--color-text-secondary)}.StickerSearch .sticker-set-header .Button.is-added:hover,.StickerSearch .sticker-set-header .Button.is-added:active{background:var(--color-item-active) !important}.StickerSearch .sticker-set-main{display:flex;flex-wrap:nowrap;overflow:hidden}.StickerSearch .sticker-set[dir=rtl] .title-wrapper{text-align:right}.StickerSearch .sticker-set[dir=rtl] .Button{margin-left:0;margin-right:1rem}.StickerSearch .StickerButton{margin:0.125rem} 2 | -------------------------------------------------------------------------------- /src/components/right/hooks/usePhotosPreload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiChat, ApiMediaFormat, ApiPhoto, ApiUser, 3 | } from '../../../api/types'; 4 | import { useEffect } from '../../../lib/teact/teact'; 5 | import * as mediaLoader from '../../../util/mediaLoader'; 6 | 7 | const PHOTOS_TO_PRELOAD = 4; 8 | 9 | export default function usePhotosPreload( 10 | profile: ApiUser | ApiChat | undefined, 11 | photos: ApiPhoto[], 12 | currentIndex: number, 13 | ) { 14 | useEffect(() => { 15 | photos.slice(currentIndex, currentIndex + PHOTOS_TO_PRELOAD).forEach((photo) => { 16 | const mediaData = mediaLoader.getFromMemory(`photo${photo.id}?size=c`); 17 | if (!mediaData) { 18 | mediaLoader.fetch(`photo${photo.id}?size=c`, ApiMediaFormat.BlobUrl); 19 | } 20 | }); 21 | }, [currentIndex, photos]); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/right/management/Management.async.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../../lib/teact/teact'; 2 | import { Bundles } from '../../../util/moduleLoader'; 3 | 4 | import { OwnProps } from './Management'; 5 | 6 | import useModuleLoader from '../../../hooks/useModuleLoader'; 7 | 8 | import Loading from '../../ui/Loading'; 9 | 10 | const ManagementAsync: FC = (props) => { 11 | const Management = useModuleLoader(Bundles.Extra, 'Management'); 12 | 13 | // eslint-disable-next-line react/jsx-props-no-spreading 14 | return Management ? : ; 15 | }; 16 | 17 | export default memo(ManagementAsync); 18 | -------------------------------------------------------------------------------- /src/components/ui/DropdownMenu.scss: -------------------------------------------------------------------------------- 1 | .DropdownMenu{position:relative} 2 | -------------------------------------------------------------------------------- /src/components/ui/FloatingActionButton.scss: -------------------------------------------------------------------------------- 1 | .FloatingActionButton{position:absolute;right:1rem;bottom:1rem;transform:translateY(5rem);transition:transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)}body.animation-level-0 .FloatingActionButton{transition:none !important}.FloatingActionButton.revealed{transform:translateY(0)} 2 | -------------------------------------------------------------------------------- /src/components/ui/FloatingActionButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../lib/teact/teact'; 2 | 3 | import buildClassName from '../../util/buildClassName'; 4 | 5 | import Button, { OwnProps as ButtonProps } from './Button'; 6 | 7 | import './FloatingActionButton.scss'; 8 | 9 | type OwnProps = { 10 | isShown: boolean; 11 | className?: string; 12 | color?: ButtonProps['color']; 13 | ariaLabel?: ButtonProps['ariaLabel']; 14 | disabled?: boolean; 15 | onClick: () => void; 16 | children: any; 17 | }; 18 | 19 | const FloatingActionButton: FC = ({ 20 | isShown, 21 | className, 22 | color = 'primary', 23 | ariaLabel, 24 | disabled, 25 | onClick, 26 | children, 27 | }) => { 28 | const buttonClassName = buildClassName( 29 | 'FloatingActionButton', 30 | isShown && 'revealed', 31 | className, 32 | ); 33 | 34 | return ( 35 | 46 | ); 47 | }; 48 | 49 | export default FloatingActionButton; 50 | -------------------------------------------------------------------------------- /src/components/ui/Link.scss: -------------------------------------------------------------------------------- 1 | .Link{color:inherit}.Link:hover{color:inherit} 2 | -------------------------------------------------------------------------------- /src/components/ui/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from '../../lib/teact/teact'; 2 | 3 | import buildClassName from '../../util/buildClassName'; 4 | 5 | import './Link.scss'; 6 | 7 | type OwnProps = { 8 | children: any; 9 | className?: string; 10 | isRtl?: boolean; 11 | onClick?: (e: React.MouseEvent) => void; 12 | }; 13 | 14 | const Link: FC = ({ 15 | children, className, isRtl, onClick, 16 | }) => { 17 | const handleClick = useCallback((e: React.MouseEvent) => { 18 | e.preventDefault(); 19 | onClick!(e); 20 | }, [onClick]); 21 | 22 | return ( 23 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | export default Link; 35 | -------------------------------------------------------------------------------- /src/components/ui/Loading.scss: -------------------------------------------------------------------------------- 1 | .Loading{display:flex;height:100%;align-items:center;justify-content:center}.Loading .Spinner{--spinner-size: 2.75rem} 2 | -------------------------------------------------------------------------------- /src/components/ui/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from '../../lib/teact/teact'; 2 | 3 | import Spinner from './Spinner'; 4 | 5 | import './Loading.scss'; 6 | 7 | type OwnProps = { 8 | color?: 'blue' | 'white' | 'black'; 9 | }; 10 | 11 | const Loading: FC = ({ color = 'blue' }) => { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default memo(Loading); 20 | -------------------------------------------------------------------------------- /src/components/ui/Menu.scss: -------------------------------------------------------------------------------- 1 | .Menu.fluid .bubble{min-width:13.5rem;width:auto}.Menu .backdrop{position:fixed;left:-100vw;right:-100vw;top:-100vh;bottom:-100vh;z-index:var(--z-menu-backdrop)}.Menu .bubble{overflow:hidden;display:block;list-style:none;padding:0.5rem 0;margin:0;position:absolute;background-color:var(--color-background);box-shadow:0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);border-radius:var(--border-radius-default);min-width:13.5rem;z-index:var(--z-menu-bubble);transform:scale(0.2);transition:opacity 0.2s cubic-bezier(0.2, 0, 0.2, 1),transform 0.2s cubic-bezier(0.2, 0, 0.2, 1) !important;--offset-y: calc(100% + 0.5rem);--offset-x: 0}.Menu .bubble.open{transform:scale(1)}.Menu .bubble.closing{transition:opacity .2s ease-in, transform .2s ease-in !important}body.animation-level-0 .Menu .bubble{transform:none !important;transition:opacity .15s !important}body.has-open-dialog .Menu .bubble{transition:none !important}.Menu .bubble.top{top:var(--offset-y)}.Menu .bubble.bottom{bottom:var(--offset-y)}.Menu .bubble.left{left:var(--offset-x)}.Menu .bubble.right{right:var(--offset-x)}.Menu .bubble.with-footer{padding-bottom:0}.Menu .footer{padding:0.5rem 0;background:var(--color-chat-hover);color:var(--color-text-secondary);font-size:0.8125rem;text-align:center} 2 | -------------------------------------------------------------------------------- /src/components/ui/MenuItem.scss: -------------------------------------------------------------------------------- 1 | .MenuItem{width:100%;background:none;border:none !important;box-shadow:none !important;outline:none !important;display:flex;padding:1rem;position:relative;overflow:hidden;line-height:1.5rem;white-space:nowrap;color:var(--color-text);--ripple-color: rgba(0, 0, 0, .08);cursor:pointer;unicode-bidi:plaintext}@media (hover: hover){.MenuItem:hover,.MenuItem:focus{background-color:var(--color-chat-hover);text-decoration:none;color:inherit}}@media (max-width: 600px){.MenuItem:focus,.MenuItem:hover,.MenuItem:active{text-decoration:none;color:inherit}.MenuItem:active{background-color:var(--color-chat-hover)}}.MenuItem i{font-size:1.5rem;margin-right:2rem;color:var(--color-text-secondary)}.MenuItem .menu-item-name{margin-right:2rem}.MenuItem .menu-item-name.capitalize{text-transform:capitalize}.MenuItem.disabled{opacity:0.5 !important;cursor:default !important}.MenuItem.destructive{color:var(--color-error)}.MenuItem.destructive i{color:inherit}.MenuItem:not(.has-ripple):not(.disabled):active{background-color:var(--color-item-active);transition:none !important}.MenuItem>.Switcher{margin-left:auto}.MenuItem[dir=rtl] i{margin-left:2rem;margin-right:0}.MenuItem[dir=rtl] .menu-item-name{margin-left:2rem;margin-right:0}.MenuItem[dir=rtl]>.Switcher{margin-left:0;margin-right:auto} 2 | -------------------------------------------------------------------------------- /src/components/ui/Portal.ts: -------------------------------------------------------------------------------- 1 | import { FC, useRef, useLayoutEffect } from '../../lib/teact/teact'; 2 | import TeactDOM from '../../lib/teact/teact-dom'; 3 | 4 | type OwnProps = { 5 | containerId?: string; 6 | className?: string; 7 | children: any; 8 | }; 9 | 10 | const Portal: FC = ({ containerId, className, children }) => { 11 | const elementRef = useRef(document.createElement('div')); 12 | 13 | useLayoutEffect(() => { 14 | const container = document.querySelector(containerId || '#portals'); 15 | if (!container) { 16 | return undefined; 17 | } 18 | 19 | const element = elementRef.current; 20 | if (className) { 21 | element.classList.add(className); 22 | } 23 | 24 | container.appendChild(element); 25 | 26 | return () => { 27 | TeactDOM.render(undefined, element); 28 | container.removeChild(element); 29 | }; 30 | }, [className, containerId]); 31 | 32 | return TeactDOM.render(children, elementRef.current); 33 | }; 34 | 35 | export default Portal; 36 | -------------------------------------------------------------------------------- /src/components/ui/RippleEffect.scss: -------------------------------------------------------------------------------- 1 | @keyframes ripple-animation{from{transform:scale(0);opacity:1}50%{opacity:1}to{opacity:0;transform:scale(2)}}.ripple-container{position:absolute;top:0;left:0;bottom:0;right:0}body.animation-level-0 .ripple-container{display:none}.ripple-container span{position:absolute;display:block;background-color:var(--ripple-color, rgba(0,0,0,0.08));border-radius:50%;transform:scale(0);animation:ripple-animation 700ms} 2 | -------------------------------------------------------------------------------- /src/components/ui/ShowMoreButton.scss: -------------------------------------------------------------------------------- 1 | .ShowMoreButton{color:var(--text-color) !important;display:flex;align-items:center;text-align:left;text-transform:none;padding-left:.75rem !important;opacity:1 !important}.ShowMoreButton i{font-size:1.5rem;margin-right:2rem;color:var(--color-text-secondary)}.ShowMoreButton .Spinner{top:0.4375rem} 2 | -------------------------------------------------------------------------------- /src/components/ui/ShowMoreButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../lib/teact/teact'; 2 | 3 | import useLang from '../../hooks/useLang'; 4 | 5 | import Button from './Button'; 6 | 7 | import './ShowMoreButton.scss'; 8 | 9 | type OwnProps = { 10 | count: number; 11 | itemName: string; 12 | itemPluralName?: string; 13 | isLoading?: boolean; 14 | onClick: () => void; 15 | }; 16 | 17 | const ShowMoreButton: FC = ({ 18 | count, 19 | itemName, 20 | itemPluralName, 21 | isLoading, 22 | onClick, 23 | }) => { 24 | const lang = useLang(); 25 | 26 | return ( 27 | 39 | ); 40 | }; 41 | 42 | export default ShowMoreButton; 43 | -------------------------------------------------------------------------------- /src/components/ui/Spinner.scss: -------------------------------------------------------------------------------- 1 | .Spinner{--spinner-size: 2rem;position:relative;display:flex;align-items:center;justify-content:center;width:var(--spinner-size);height:var(--spinner-size)}.Spinner>div{position:absolute;top:0;left:0;right:0;bottom:0;background-repeat:no-repeat;background-size:100%;animation-name:spin;animation-duration:1s;animation-iteration-count:infinite;animation-timing-function:linear}.Spinner.with-background::before{content:'';position:absolute;left:-0.125rem;top:-0.125rem;bottom:-0.125rem;right:-0.125rem;border-radius:50%;background:rgba(0,0,0,0.25)}.Spinner.white>div{background-image:var(--spinner-white-data)}.Spinner.white.with-background>div{background-image:var(--spinner-white-thin-data)}.Spinner.blue>div{background-image:var(--spinner-blue-data)}.theme-dark .Spinner.blue>div{background-image:var(--spinner-dark-blue-data)}.Spinner.black>div{background-image:var(--spinner-black-data)}.Spinner.green>div{background-image:var(--spinner-green-data)}.Spinner.gray>div{background-image:var(--spinner-gray-data)}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} 2 | -------------------------------------------------------------------------------- /src/components/ui/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from '../../lib/teact/teact'; 2 | 3 | import buildClassName from '../../util/buildClassName'; 4 | 5 | import './Spinner.scss'; 6 | 7 | const Spinner: FC<{ 8 | color?: 'blue' | 'white' | 'black' | 'green' | 'gray'; 9 | withBackground?: boolean; 10 | }> = ({ 11 | color = 'blue', 12 | withBackground, 13 | }) => { 14 | return ( 15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Spinner; 22 | -------------------------------------------------------------------------------- /src/components/ui/Switcher.scss: -------------------------------------------------------------------------------- 1 | .Switcher{display:inline-flex;align-items:center;position:relative;margin:0}.Switcher.disabled{pointer-events:none;opacity:0.5}.Switcher.inactive{pointer-events:none}body.animation-level-0 .Switcher .widget,body.animation-level-0 .Switcher .widget::after,.Switcher.no-animation .widget,.Switcher.no-animation .widget::after{transition:none !important}body.animation-level-0 .Switcher .widget:active:after,.Switcher.no-animation .widget:active:after{width:1.125rem}.Switcher input{height:0;width:0;visibility:hidden;position:absolute;z-index:var(--z-below);opacity:0}.Switcher .widget{cursor:pointer;text-indent:-999px;width:2.125rem;height:0.875rem;background:var(--color-gray);display:inline-block;border-radius:.5rem;position:relative;transition:background .2s ease-in}.Switcher .widget:after{content:'';position:absolute;top:-.125rem;left:0;width:1.125rem;height:1.125rem;background:var(--color-background);border-radius:.75rem;transition:0.2s ease-out;border:0.125rem solid var(--color-gray)}.Switcher input:checked+.widget{background:var(--color-primary)}.Switcher input:checked+.widget:after{left:calc(100% - 1.125rem);transform:translateX(calc(-100% + 1.125rem));border-color:var(--color-primary)}.Switcher .widget:active:after{width:1.25rem} 2 | -------------------------------------------------------------------------------- /src/components/ui/TabList.scss: -------------------------------------------------------------------------------- 1 | .TabList{position:sticky;top:0;flex-shrink:0;display:flex;justify-content:space-between;align-items:flex-end;font-size:.875rem;flex-wrap:nowrap;box-shadow:0 2px 2px var(--color-light-shadow);background-color:var(--color-background);overflow-x:auto;overflow-y:hidden;scrollbar-width:none;scrollbar-color:rgba(0,0,0,0)}.TabList.big{font-size:1rem;--border-radius-messages-small: 0}.TabList::-webkit-scrollbar{height:0}.TabList::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,0)} 2 | -------------------------------------------------------------------------------- /src/global/index.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../lib/teact/teactn'; 2 | 3 | import { INITIAL_STATE } from './initial'; 4 | import { initCache, loadCache } from './cache'; 5 | import { cloneDeep } from '../util/iteratees'; 6 | 7 | initCache(); 8 | 9 | addReducer('init', () => { 10 | const initial = cloneDeep(INITIAL_STATE); 11 | return loadCache(initial) || initial; 12 | }); 13 | -------------------------------------------------------------------------------- /src/hooks/useBackgroundMode.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | 3 | export default function useBackgroundMode( 4 | onBlur?: AnyToVoidFunction, 5 | onFocus?: AnyToVoidFunction, 6 | ) { 7 | useEffect(() => { 8 | if (onBlur && !document.hasFocus()) { 9 | onBlur(); 10 | } 11 | 12 | if (onBlur) { 13 | window.addEventListener('blur', onBlur); 14 | } 15 | 16 | if (onFocus) { 17 | window.addEventListener('focus', onFocus); 18 | } 19 | 20 | return () => { 21 | if (onFocus) { 22 | window.removeEventListener('focus', onFocus); 23 | } 24 | 25 | if (onBlur) { 26 | window.removeEventListener('blur', onBlur); 27 | } 28 | }; 29 | }, [onBlur, onFocus]); 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useBeforeUnload.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | 3 | import { onBeforeUnload } from '../util/schedulers'; 4 | 5 | export default function useBeforeUnload(callback: AnyToVoidFunction) { 6 | useEffect(() => { 7 | return onBeforeUnload(callback); 8 | }, [callback]); 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useBrowserOnline.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from '../lib/teact/teact'; 2 | 3 | export default function useBrowserOnline() { 4 | const [isOnline, setIsOnline] = useState(window.navigator.onLine); 5 | 6 | useEffect(() => { 7 | function handleChange() { 8 | setIsOnline(window.navigator.onLine); 9 | } 10 | 11 | window.addEventListener('online', handleChange); 12 | window.addEventListener('offline', handleChange); 13 | 14 | return () => { 15 | window.removeEventListener('offline', handleChange); 16 | window.removeEventListener('online', handleChange); 17 | }; 18 | }, []); 19 | 20 | return isOnline; 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useCacheBuster.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from '../lib/teact/teact'; 2 | 3 | export default () => { 4 | const [cacheBuster, setCacheBuster] = useState(false); 5 | 6 | const updateCacheBuster = useCallback(() => { 7 | setCacheBuster((current) => !current); 8 | }, []); 9 | 10 | return [cacheBuster, updateCacheBuster] as const; 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/useCurrentOrPrev.ts: -------------------------------------------------------------------------------- 1 | import usePrevious from './usePrevious'; 2 | 3 | export default function useCurrentOrPrev( 4 | current: T, shouldSkipUndefined = false, shouldForceCurrent = false, 5 | ): T | undefined { 6 | const prev = usePrevious(current, shouldSkipUndefined); 7 | 8 | // eslint-disable-next-line no-null/no-null 9 | return shouldForceCurrent || (current !== null && current !== undefined) ? current : prev; 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/useCustomBackground.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from '../lib/teact/teact'; 2 | 3 | import { ThemeKey } from '../types'; 4 | 5 | import { CUSTOM_BG_CACHE_NAME } from '../config'; 6 | import * as cacheApi from '../util/cacheApi'; 7 | import { preloadImage } from '../util/files'; 8 | 9 | export default (theme: ThemeKey, settingValue?: string) => { 10 | const [value, setValue] = useState(settingValue); 11 | 12 | useEffect(() => { 13 | if (!settingValue) { 14 | return; 15 | } 16 | 17 | if (settingValue.startsWith('#')) { 18 | setValue(settingValue); 19 | } else { 20 | cacheApi.fetch(CUSTOM_BG_CACHE_NAME, theme, cacheApi.Type.Blob) 21 | .then((blob) => { 22 | const url = URL.createObjectURL(blob); 23 | preloadImage(url) 24 | .then(() => { 25 | setValue(`url(${url})`); 26 | }); 27 | }); 28 | } 29 | }, [settingValue, theme]); 30 | 31 | return settingValue ? value : undefined; 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from '../lib/teact/teact'; 2 | 3 | import { debounce } from '../util/schedulers'; 4 | 5 | export default function useDebounce(ms: number, shouldRunFirst?: boolean, shouldRunLast?: boolean) { 6 | return useMemo(() => { 7 | return debounce((cb) => cb(), ms, shouldRunFirst, shouldRunLast); 8 | }, [ms, shouldRunFirst, shouldRunLast]); 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/useEffectWithPrevDeps.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | import usePrevious from './usePrevious'; 3 | 4 | export default (cb: (args: PT) => void, dependencies: T, debugKey?: string) => { 5 | const prevDeps = usePrevious(dependencies); 6 | return useEffect(() => { 7 | // @ts-ignore (workaround for "could be instantiated with a different subtype" issue) 8 | return cb(prevDeps || []); 9 | // eslint-disable-next-line react-hooks/exhaustive-deps 10 | }, dependencies, debugKey); 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/useEnsureMessage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from '../lib/teact/teact'; 2 | import { getDispatch } from '../lib/teact/teactn'; 3 | 4 | import { ApiMessage } from '../api/types'; 5 | 6 | import { throttle } from '../util/schedulers'; 7 | 8 | export default ( 9 | chatId: number, 10 | messageId?: number, 11 | message?: ApiMessage, 12 | replyOriginForId?: number, 13 | ) => { 14 | const { loadMessage } = getDispatch(); 15 | const loadMessageThrottled = useMemo(() => { 16 | const throttled = throttle(loadMessage, 500, true); 17 | return () => { 18 | throttled({ chatId, messageId, replyOriginForId }); 19 | }; 20 | }, [loadMessage, chatId, messageId, replyOriginForId]); 21 | 22 | useEffect(() => { 23 | if (messageId && !message) { 24 | loadMessageThrottled(); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useFlag.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from '../lib/teact/teact'; 2 | 3 | export default (initial = false): [boolean, AnyToVoidFunction, AnyToVoidFunction] => { 4 | const [value, setValue] = useState(initial); 5 | 6 | const setTrue = useCallback(() => { 7 | setValue(true); 8 | }, []); 9 | 10 | const setFalse = useCallback(() => { 11 | setValue(false); 12 | }, []); 13 | 14 | return [value, setTrue, setFalse]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/hooks/useFocusAfterAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | import { IS_TOUCH_ENV } from '../util/environment'; 4 | import { fastRaf } from '../util/schedulers'; 5 | import { useEffect } from '../lib/teact/teact'; 6 | 7 | const DEFAULT_DURATION = 400; 8 | 9 | export default function useFocusAfterAnimation( 10 | ref: RefObject, animationDuration = DEFAULT_DURATION, 11 | ) { 12 | useEffect(() => { 13 | if (IS_TOUCH_ENV) { 14 | return; 15 | } 16 | 17 | setTimeout(() => { 18 | fastRaf(() => { 19 | if (ref.current) { 20 | ref.current.focus(); 21 | } 22 | }); 23 | }, animationDuration); 24 | }, [ref, animationDuration]); 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from '../lib/teact/teact'; 2 | 3 | export default () => { 4 | const [, setTrigger] = useState(false); 5 | 6 | return useCallback(() => { 7 | setTrigger((trigger) => !trigger); 8 | }, []); 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/useHeavyAnimationCheckForVideo.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { useCallback, useRef } from '../lib/teact/teact'; 3 | 4 | import useHeavyAnimationCheck from './useHeavyAnimationCheck'; 5 | import safePlay from '../util/safePlay'; 6 | 7 | export default function useHeavyAnimationCheckForVideo(playerRef: RefObject, shouldPlay: boolean) { 8 | const shouldPlayRef = useRef(); 9 | shouldPlayRef.current = shouldPlay; 10 | 11 | const pause = useCallback(() => { 12 | if (playerRef.current) { 13 | playerRef.current.pause(); 14 | } 15 | }, [playerRef]); 16 | 17 | const play = useCallback(() => { 18 | if (playerRef.current && shouldPlayRef.current) { 19 | safePlay(playerRef.current); 20 | } 21 | }, [playerRef]); 22 | 23 | useHeavyAnimationCheck(pause, play); 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useHorizontalScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | 3 | export default (container: HTMLElement | null, isDisabled?: boolean) => { 4 | useEffect(() => { 5 | if (!container) { 6 | return undefined; 7 | } 8 | 9 | function handleScroll(e: WheelEvent) { 10 | // Ignore horizontal scroll and let it work natively (e.g. on touchpad) 11 | if (!e.deltaX) { 12 | container!.scrollLeft += e.deltaY / 4; 13 | } 14 | } 15 | 16 | container.addEventListener('wheel', handleScroll, { passive: true }); 17 | 18 | return () => { 19 | container.removeEventListener('wheel', handleScroll); 20 | }; 21 | }, [container, isDisabled]); 22 | }; 23 | -------------------------------------------------------------------------------- /src/hooks/useLang.ts: -------------------------------------------------------------------------------- 1 | import { ApiMediaFormat } from '../api/types'; 2 | 3 | import * as langProvider from '../util/langProvider'; 4 | import useForceUpdate from './useForceUpdate'; 5 | import useOnChange from './useOnChange'; 6 | 7 | export type LangFn = typeof langProvider.getTranslation; 8 | 9 | export default (): LangFn => { 10 | const forceUpdate = useForceUpdate(); 11 | 12 | useOnChange(() => { 13 | return langProvider.addCallback(forceUpdate); 14 | }, [forceUpdate]); 15 | 16 | return langProvider.getTranslation; 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/useLangString.ts: -------------------------------------------------------------------------------- 1 | import * as langProvider from '../util/langProvider'; 2 | import { useState } from '../lib/teact/teact'; 3 | 4 | export default (langCode: string | undefined, key: string): string | undefined => { 5 | const [translation, setTranslation] = useState(); 6 | 7 | if (langCode) { 8 | langProvider 9 | .getTranslationForLangString(langCode, key) 10 | .then(setTranslation); 11 | } 12 | 13 | return translation; 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/useLayoutEffectWithPrevDeps.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from '../lib/teact/teact'; 2 | import usePrevious from './usePrevious'; 3 | 4 | export default (cb: (args: PT) => void, dependencies: T, debugKey?: string) => { 5 | const prevDeps = usePrevious(dependencies); 6 | return useLayoutEffect(() => { 7 | // @ts-ignore (workaround for "could be instantiated with a different subtype" issue) 8 | return cb(prevDeps || []); 9 | // eslint-disable-next-line react-hooks/exhaustive-deps 10 | }, dependencies, debugKey); 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/useMedia.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | 3 | import { ApiMediaFormat } from '../api/types'; 4 | 5 | import * as mediaLoader from '../util/mediaLoader'; 6 | import useForceUpdate from './useForceUpdate'; 7 | 8 | export default ( 9 | mediaHash: string | false | undefined, 10 | noLoad = false, 11 | // @ts-ignore (workaround for "could be instantiated with a different subtype" issue) 12 | mediaFormat: T = ApiMediaFormat.BlobUrl, 13 | cacheBuster?: number, 14 | delay?: number | false, 15 | ) => { 16 | const mediaData = mediaHash ? mediaLoader.getFromMemory(mediaHash) : undefined; 17 | const forceUpdate = useForceUpdate(); 18 | 19 | useEffect(() => { 20 | if (!noLoad && mediaHash && !mediaData) { 21 | const startedAt = Date.now(); 22 | 23 | mediaLoader.fetch(mediaHash, mediaFormat).then(() => { 24 | const spentTime = Date.now() - startedAt; 25 | if (!delay || spentTime >= delay) { 26 | forceUpdate(); 27 | } else { 28 | setTimeout(forceUpdate, delay - spentTime); 29 | } 30 | }); 31 | } 32 | }, [noLoad, mediaHash, mediaData, mediaFormat, cacheBuster, forceUpdate, delay]); 33 | 34 | return mediaData; 35 | }; 36 | -------------------------------------------------------------------------------- /src/hooks/useMediaDownload.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from '../lib/teact/teact'; 2 | 3 | import useMediaWithDownloadProgress from './useMediaWithDownloadProgress'; 4 | import download from '../util/download'; 5 | 6 | export default function useMediaDownload( 7 | mediaHash?: string, 8 | fileName?: string, 9 | ) { 10 | const [isDownloadStarted, setIsDownloadStarted] = useState(false); 11 | 12 | const { mediaData, downloadProgress } = useMediaWithDownloadProgress(mediaHash, !isDownloadStarted); 13 | 14 | // Download with browser when fully loaded 15 | useEffect(() => { 16 | if (isDownloadStarted && mediaData) { 17 | download(mediaData, fileName!); 18 | setIsDownloadStarted(false); 19 | } 20 | }, [fileName, mediaData, isDownloadStarted]); 21 | 22 | // Cancel download on source change 23 | useEffect(() => { 24 | setIsDownloadStarted(false); 25 | }, [mediaHash]); 26 | 27 | const handleDownloadClick = useCallback((e: React.SyntheticEvent) => { 28 | e.stopPropagation(); 29 | setIsDownloadStarted((isAllowed) => !isAllowed); 30 | }, []); 31 | 32 | return { 33 | isDownloadStarted, 34 | downloadProgress, 35 | handleDownloadClick, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/useModuleLoader.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/teact/teact'; 2 | 3 | import { 4 | BundleModules, Bundles, getModuleFromMemory, loadModule, addLoadListener, 5 | } from '../util/moduleLoader'; 6 | 7 | import useForceUpdate from './useForceUpdate'; 8 | 9 | export default >( 10 | bundleName: B, moduleName: M, noLoad = false, autoUpdate = false, 11 | ) => { 12 | const module = getModuleFromMemory(bundleName, moduleName); 13 | const forceUpdate = useForceUpdate(); 14 | 15 | if (autoUpdate) { 16 | // Use effect and cleanup for listener removal 17 | addLoadListener(forceUpdate); 18 | } 19 | 20 | useEffect(() => { 21 | if (!noLoad && !module) { 22 | loadModule(bundleName, moduleName).then(forceUpdate); 23 | } 24 | }, [bundleName, forceUpdate, module, moduleName, noLoad]); 25 | 26 | return module; 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useOnChange.ts: -------------------------------------------------------------------------------- 1 | import usePrevious from './usePrevious'; 2 | 3 | export default (cb: (args: PT) => void, dependencies: T) => { 4 | const prevDeps = usePrevious(dependencies); 5 | if (!prevDeps || dependencies.some((d, i) => d !== prevDeps[i])) { 6 | // @ts-ignore (workaround for "could be instantiated with a different subtype" issue) 7 | cb(prevDeps || []); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/usePrevDuringAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from '../lib/teact/teact'; 2 | 3 | import usePrevious from './usePrevious'; 4 | import useForceUpdate from './useForceUpdate'; 5 | import useOnChange from './useOnChange'; 6 | 7 | export default function usePrevDuringAnimation(current: any, duration?: number) { 8 | const prev = usePrevious(current, true); 9 | const timeoutRef = useRef(); 10 | const forceUpdate = useForceUpdate(); 11 | // eslint-disable-next-line no-null/no-null 12 | const isCurrentPresent = current !== undefined && current !== null; 13 | // eslint-disable-next-line no-null/no-null 14 | const isPrevPresent = prev !== undefined && prev !== null; 15 | 16 | if (isCurrentPresent && timeoutRef.current) { 17 | clearTimeout(timeoutRef.current); 18 | timeoutRef.current = undefined; 19 | } 20 | 21 | useOnChange(() => { 22 | // When `current` becomes empty 23 | if (duration && !isCurrentPresent && isPrevPresent && !timeoutRef.current) { 24 | timeoutRef.current = window.setTimeout(() => { 25 | timeoutRef.current = undefined; 26 | forceUpdate(); 27 | }, duration); 28 | } 29 | }, [current]); 30 | 31 | return !timeoutRef.current || !duration || isCurrentPresent ? current : prev; 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from '../lib/teact/teact'; 2 | 3 | function usePrevious(next: T): T | undefined; 4 | function usePrevious(next: T, shouldSkipUndefined: true): Exclude | undefined; 5 | function usePrevious(next: T, shouldSkipUndefined?: boolean): Exclude | undefined; 6 | function usePrevious(next: T, shouldSkipUndefined?: boolean) { 7 | const ref = useRef(); 8 | const { current } = ref; 9 | if (!shouldSkipUndefined || next !== undefined) { 10 | ref.current = next; 11 | } 12 | 13 | return current; 14 | } 15 | 16 | export default usePrevious; 17 | -------------------------------------------------------------------------------- /src/hooks/useReducer.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef } from '../lib/teact/teact'; 2 | 3 | export type ReducerAction = { type: Actions; payload?: any }; 4 | export type StateReducer = (state: State, action: ReducerAction) => State; 5 | export type Dispatch = (action: ReducerAction) => void; 6 | 7 | export default function useReducer( 8 | reducer: StateReducer, 9 | initialState: State, 10 | ) { 11 | const reducerRef = useRef(reducer); 12 | const [state, setState] = useState(initialState); 13 | 14 | const dispatch = useCallback((action: ReducerAction) => { 15 | setState((currentState) => reducerRef.current(currentState, action)); 16 | }, []); 17 | 18 | return [ 19 | state, 20 | dispatch, 21 | ] as [State, Dispatch]; 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useSelectWithEnter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from '../lib/teact/teact'; 2 | 3 | export default ( 4 | onSelect: NoneToVoidFunction, 5 | ) => { 6 | // eslint-disable-next-line no-null/no-null 7 | const buttonRef = useRef(null); 8 | 9 | const handleKeyDown = useCallback((e: KeyboardEvent) => { 10 | if (e.key !== 'Enter') return; 11 | const isFocused = buttonRef.current === document.activeElement; 12 | 13 | if (isFocused) { 14 | onSelect(); 15 | } 16 | }, [onSelect]); 17 | 18 | useEffect(() => { 19 | window.addEventListener('keydown', handleKeyDown, false); 20 | 21 | return () => window.removeEventListener('keydown', handleKeyDown); 22 | }, [handleKeyDown]); 23 | 24 | return buttonRef; 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from '../lib/teact/teact'; 2 | 3 | import { throttle } from '../util/schedulers'; 4 | 5 | export default (ms: number, noFirst = false) => { 6 | return useMemo(() => { 7 | return throttle((cb) => cb(), ms, !noFirst); 8 | }, [ms, noFirst]); 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/useThrottledMemo.ts: -------------------------------------------------------------------------------- 1 | import { useState } from '../lib/teact/teact'; 2 | 3 | import useThrottle from './useThrottle'; 4 | import useOnChange from './useOnChange'; 5 | import useHeavyAnimationCheck from './useHeavyAnimationCheck'; 6 | import useFlag from './useFlag'; 7 | 8 | export default (resolverFn: () => R, ms: number, dependencies: D) => { 9 | const runThrottled = useThrottle(ms, true); 10 | const [value, setValue] = useState(); 11 | const [isFrozen, freeze, unfreeze] = useFlag(); 12 | 13 | useHeavyAnimationCheck(freeze, unfreeze); 14 | 15 | useOnChange(() => { 16 | if (isFrozen) { 17 | return; 18 | } 19 | 20 | runThrottled(() => { 21 | setValue(resolverFn()); 22 | }); 23 | }, dependencies.concat([isFrozen])); 24 | 25 | return value; 26 | }; 27 | -------------------------------------------------------------------------------- /src/hooks/useTransitionForMedia.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from '../lib/teact/teact'; 2 | 3 | import useShowTransition from './useShowTransition'; 4 | 5 | const SPEED = { 6 | fast: 200, 7 | slow: 350, 8 | }; 9 | 10 | export default (mediaData?: any, speed: keyof typeof SPEED = 'fast', noAnimate = false) => { 11 | const isMediaLoaded = Boolean(mediaData); 12 | const willAnimate = !useRef(isMediaLoaded).current && !noAnimate; 13 | const [shouldRenderThumb, setShouldRenderThumb] = useState(!isMediaLoaded); 14 | 15 | const { 16 | shouldRender: shouldRenderFullMedia, 17 | transitionClassNames, 18 | } = useShowTransition(isMediaLoaded, undefined, !willAnimate, speed); 19 | 20 | useEffect(() => { 21 | if (shouldRenderFullMedia) { 22 | if (willAnimate) { 23 | setTimeout(() => { 24 | setShouldRenderThumb(false); 25 | }, SPEED[speed]); 26 | } else { 27 | setShouldRenderThumb(false); 28 | } 29 | } 30 | }, [willAnimate, shouldRenderFullMedia, speed]); 31 | 32 | return { 33 | shouldRenderThumb, 34 | shouldRenderFullMedia, 35 | transitionClassNames, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/hooks/useVideoCleanup.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { useEffect } from '../lib/teact/teact'; 3 | import { fastRaf } from '../util/schedulers'; 4 | 5 | // Fix for memory leak when unmounting video element 6 | export default function useVideoCleanup(videoRef: RefObject, dependencies: any[]) { 7 | useEffect(() => { 8 | const videoEl = videoRef.current; 9 | 10 | return () => { 11 | if (videoEl) { 12 | fastRaf(() => { 13 | videoEl.pause(); 14 | videoEl.src = ''; 15 | videoEl.load(); 16 | }); 17 | } 18 | }; 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, dependencies); 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useVirtualBackdrop.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { useEffect } from '../lib/teact/teact'; 3 | 4 | const BACKDROP_CLASSNAME = 'backdrop'; 5 | 6 | // This effect implements closing menus by clicking outside of them 7 | // without adding extra elements to the DOM 8 | export default function useVirtualBackdrop( 9 | isOpen: boolean, 10 | menuRef: RefObject, 11 | onClose?: () => void | undefined, 12 | ) { 13 | useEffect(() => { 14 | const handleEvent = (e: Event) => { 15 | const menu = menuRef.current; 16 | const target = e.target as HTMLElement | null; 17 | if (!menu || !target) { 18 | return; 19 | } 20 | 21 | if ( 22 | !menu.contains(e.target as Node | null) 23 | || target.classList.contains(BACKDROP_CLASSNAME) 24 | ) { 25 | e.preventDefault(); 26 | e.stopPropagation(); 27 | if (onClose) { 28 | onClose(); 29 | } 30 | } 31 | }; 32 | 33 | if (isOpen && onClose) { 34 | document.addEventListener('mousedown', handleEvent); 35 | } 36 | 37 | return () => { 38 | document.removeEventListener('mousedown', handleEvent); 39 | }; 40 | }, [isOpen, menuRef, onClose]); 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from '../lib/teact/teact'; 2 | 3 | import { throttle } from '../util/schedulers'; 4 | import windowSize from '../util/windowSize'; 5 | import { ApiDimensions } from '../api/types'; 6 | 7 | const THROTTLE = 250; 8 | 9 | export default () => { 10 | const [size, setSize] = useState(windowSize.get()); 11 | 12 | useEffect(() => { 13 | const handleResize = throttle(() => { 14 | setSize(windowSize.get()); 15 | }, THROTTLE, false); 16 | 17 | window.addEventListener('resize', handleResize); 18 | 19 | return () => { 20 | window.removeEventListener('resize', handleResize); 21 | }; 22 | }, []); 23 | 24 | return size; 25 | }; 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import './util/handleError'; 2 | import './util/setupServiceWorker'; 3 | 4 | import React, { getDispatch, getGlobal } from './lib/teact/teactn'; 5 | import TeactDOM from './lib/teact/teact-dom'; 6 | 7 | import './global'; 8 | 9 | import { DEBUG } from './config'; 10 | 11 | import App from './App'; 12 | 13 | import './styles/index.scss'; 14 | 15 | if (DEBUG) { 16 | // eslint-disable-next-line no-console 17 | console.log('>>> INIT'); 18 | } 19 | 20 | getDispatch().init(); 21 | 22 | if (DEBUG) { 23 | // eslint-disable-next-line no-console 24 | console.log('>>> START INITIAL RENDER'); 25 | } 26 | 27 | TeactDOM.render( 28 | , 29 | document.getElementById('root'), 30 | ); 31 | 32 | if (DEBUG) { 33 | // eslint-disable-next-line no-console 34 | console.log('>>> FINISH INITIAL RENDER'); 35 | } 36 | 37 | document.addEventListener('dblclick', () => { 38 | // eslint-disable-next-line no-console 39 | console.log('GLOBAL STATE', getGlobal()); 40 | }); 41 | -------------------------------------------------------------------------------- /src/lib/gramjs/Version.js: -------------------------------------------------------------------------------- 1 | module.exports = '0.0.2'; 2 | -------------------------------------------------------------------------------- /src/lib/gramjs/crypto/CTR.js: -------------------------------------------------------------------------------- 1 | const crypto = require('./crypto'); 2 | 3 | class CTR { 4 | constructor(key, iv) { 5 | if (!Buffer.isBuffer(key) || !Buffer.isBuffer(iv) || iv.length !== 16) { 6 | throw new Error('Key and iv need to be a buffer'); 7 | } 8 | 9 | this.cipher = crypto.createCipheriv('AES-256-CTR', key, iv); 10 | } 11 | 12 | encrypt(data) { 13 | return Buffer.from(this.cipher.update(data)); 14 | } 15 | } 16 | 17 | module.exports = CTR; 18 | -------------------------------------------------------------------------------- /src/lib/gramjs/crypto/IGE.js: -------------------------------------------------------------------------------- 1 | const { IGE: AESIGE } = require('@cryptography/aes'); 2 | const Helpers = require('../Helpers'); 3 | 4 | 5 | class IGENEW { 6 | constructor(key, iv) { 7 | this.ige = new AESIGE(key, iv); 8 | } 9 | 10 | /** 11 | * Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector 12 | * @param cipherText {Buffer} 13 | * @returns {Buffer} 14 | */ 15 | decryptIge(cipherText) { 16 | return Helpers.convertToLittle(this.ige.decrypt(cipherText)); 17 | } 18 | 19 | /** 20 | * Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector 21 | * @param plainText {Buffer} 22 | * @returns {Buffer} 23 | */ 24 | encryptIge(plainText) { 25 | const padding = plainText.length % 16; 26 | if (padding) { 27 | plainText = Buffer.concat([plainText, Helpers.generateRandomBytes(16 - padding)]); 28 | } 29 | 30 | return Helpers.convertToLittle(this.ige.encrypt(plainText)); 31 | } 32 | } 33 | 34 | module.exports = IGENEW; 35 | -------------------------------------------------------------------------------- /src/lib/gramjs/errors/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a Telegram's RPC Error to a Python error. 3 | * @param rpcError the RPCError instance 4 | * @param request the request that caused this error 5 | * @constructor the RPCError as a Python exception that represents this error 6 | */ 7 | const { RPCError } = require('./RPCBaseErrors'); 8 | const { rpcErrorRe } = require('./RPCErrorList'); 9 | 10 | function RPCMessageToError(rpcError, request) { 11 | for (const [msgRegex, Cls] of rpcErrorRe) { 12 | const m = rpcError.errorMessage.match(msgRegex); 13 | if (m) { 14 | const capture = m.length === 2 ? parseInt(m[1], 10) : undefined; 15 | return new Cls({ 16 | request, 17 | capture, 18 | }); 19 | } 20 | } 21 | 22 | return new RPCError(rpcError.errorMessage, request); 23 | } 24 | 25 | const Common = require('./Common'); 26 | const RPCBaseErrors = require('./RPCBaseErrors'); 27 | const RPCErrorList = require('./RPCErrorList'); 28 | 29 | module.exports = { 30 | RPCMessageToError, 31 | ...Common, 32 | ...RPCBaseErrors, 33 | ...RPCErrorList, 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/gramjs/events/Raw.js: -------------------------------------------------------------------------------- 1 | const { EventBuilder } = require('./common'); 2 | 3 | class Raw extends EventBuilder { 4 | constructor(args = { 5 | types: undefined, 6 | func: undefined, 7 | }) { 8 | super(); 9 | if (!args.types) { 10 | this.types = true; 11 | } else { 12 | this.types = args.types; 13 | } 14 | } 15 | 16 | build(update) { 17 | return update; 18 | } 19 | } 20 | 21 | module.exports = Raw; 22 | -------------------------------------------------------------------------------- /src/lib/gramjs/events/common.js: -------------------------------------------------------------------------------- 1 | class EventBuilder { 2 | constructor(args = { 3 | chats: undefined, 4 | blacklistChats: undefined, 5 | func: undefined, 6 | }) { 7 | this.chats = args.chats; 8 | this.blacklistChats = Boolean(args.blacklistChats); 9 | this.resolved = false; 10 | this.func = args.func; 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | build(update) { 15 | 16 | } 17 | } 18 | 19 | 20 | module.exports = { 21 | EventBuilder, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/gramjs/events/index.js: -------------------------------------------------------------------------------- 1 | const NewMessage = require('./NewMessage'); 2 | const Raw = require('./Raw'); 3 | 4 | class StopPropagation extends Error { 5 | 6 | } 7 | 8 | module.exports = { 9 | NewMessage, 10 | StopPropagation, 11 | Raw, 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/gramjs/extensions/AsyncQueue.js: -------------------------------------------------------------------------------- 1 | class AsyncQueue { 2 | constructor() { 3 | this._queue = []; 4 | this.canGet = new Promise((resolve) => { 5 | this.resolveGet = resolve; 6 | }); 7 | this.canPush = true; 8 | } 9 | 10 | async push(value) { 11 | await this.canPush; 12 | this._queue.push(value); 13 | this.resolveGet(true); 14 | this.canPush = new Promise((resolve) => { 15 | this.resolvePush = resolve; 16 | }); 17 | } 18 | 19 | async pop() { 20 | await this.canGet; 21 | const returned = this._queue.pop(); 22 | this.resolvePush(true); 23 | this.canGet = new Promise((resolve) => { 24 | this.resolveGet = resolve; 25 | }); 26 | return returned; 27 | } 28 | } 29 | 30 | module.exports = AsyncQueue; 31 | -------------------------------------------------------------------------------- /src/lib/gramjs/extensions/BinaryWriter.js: -------------------------------------------------------------------------------- 1 | class BinaryWriter { 2 | constructor(stream) { 3 | this._stream = stream; 4 | } 5 | 6 | write(buffer) { 7 | this._stream = Buffer.concat([this._stream, buffer]); 8 | } 9 | 10 | getValue() { 11 | return this._stream; 12 | } 13 | } 14 | 15 | module.exports = BinaryWriter; 16 | -------------------------------------------------------------------------------- /src/lib/gramjs/extensions/index.js: -------------------------------------------------------------------------------- 1 | const Logger = require('./Logger'); 2 | const BinaryWriter = require('./BinaryWriter'); 3 | const BinaryReader = require('./BinaryReader'); 4 | const PromisedWebSockets = require('./PromisedWebSockets'); 5 | const MessagePacker = require('./MessagePacker'); 6 | const AsyncQueue = require('./AsyncQueue'); 7 | 8 | module.exports = { 9 | BinaryWriter, 10 | BinaryReader, 11 | MessagePacker, 12 | AsyncQueue, 13 | Logger, 14 | PromisedWebSockets, 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/gramjs/index.js: -------------------------------------------------------------------------------- 1 | const Api = require('./tl/api'); 2 | const TelegramClient = require('./client/TelegramClient'); 3 | const connection = require('./network'); 4 | const tl = require('./tl'); 5 | const version = require('./Version'); 6 | const events = require('./events'); 7 | const utils = require('./Utils'); 8 | const errors = require('./errors'); 9 | const sessions = require('./sessions'); 10 | const extensions = require('./extensions'); 11 | const helpers = require('./Helpers'); 12 | 13 | module.exports = { 14 | Api, 15 | TelegramClient, 16 | sessions, 17 | connection, 18 | extensions, 19 | tl, 20 | version, 21 | events, 22 | utils, 23 | errors, 24 | helpers, 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/gramjs/network/RequestState.js: -------------------------------------------------------------------------------- 1 | const { createDeferred } = require('../Helpers'); 2 | 3 | class RequestState { 4 | constructor(request, after = undefined, pending = {}) { 5 | this.containerId = undefined; 6 | this.msgId = undefined; 7 | this.request = request; 8 | this.data = request.getBytes(); 9 | this.after = after; 10 | this.result = undefined; 11 | this.pending = pending; 12 | this.deferred = createDeferred(); 13 | this.promise = new Promise((resolve, reject) => { 14 | this.resolve = resolve; 15 | this.reject = reject; 16 | }); 17 | } 18 | 19 | isReady() { 20 | const state = this.pending[this.after.id]; 21 | if (!state) { 22 | return true; 23 | } 24 | return state.deferred.promise; 25 | } 26 | } 27 | 28 | module.exports = RequestState; 29 | -------------------------------------------------------------------------------- /src/lib/gramjs/network/connection/index.js: -------------------------------------------------------------------------------- 1 | const { Connection } = require('./Connection'); 2 | const { ConnectionTCPFull } = require('./TCPFull'); 3 | const { ConnectionTCPAbridged } = require('./TCPAbridged'); 4 | const { ConnectionTCPObfuscated } = require('./TCPObfuscated'); 5 | 6 | module.exports = { 7 | Connection, 8 | ConnectionTCPFull, 9 | ConnectionTCPAbridged, 10 | ConnectionTCPObfuscated, 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/gramjs/network/index.js: -------------------------------------------------------------------------------- 1 | const MTProtoPlainSender = require('./MTProtoPlainSender'); 2 | const MTProtoSender = require('./MTProtoSender'); 3 | 4 | class UpdateConnectionState { 5 | static disconnected = -1; 6 | 7 | static connected = 1; 8 | 9 | static broken = 0; 10 | 11 | constructor(state) { 12 | this.state = state; 13 | } 14 | } 15 | 16 | class UpdateServerTimeOffset { 17 | constructor(timeOffset) { 18 | this.timeOffset = timeOffset; 19 | } 20 | } 21 | 22 | const { 23 | Connection, 24 | ConnectionTCPFull, 25 | ConnectionTCPAbridged, 26 | ConnectionTCPObfuscated, 27 | } = require('./connection'); 28 | 29 | module.exports = { 30 | Connection, 31 | ConnectionTCPFull, 32 | ConnectionTCPAbridged, 33 | ConnectionTCPObfuscated, 34 | MTProtoPlainSender, 35 | MTProtoSender, 36 | UpdateConnectionState, 37 | UpdateServerTimeOffset, 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/gramjs/sessions/CacheApiSession.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | const StorageSession = require('./StorageSession'); 3 | 4 | const CACHE_NAME = 'GramJs'; 5 | 6 | class CacheApiSession extends StorageSession { 7 | async _delete() { 8 | const request = new Request(this._storageKey); 9 | const cache = await self.caches.open(CACHE_NAME); 10 | return cache.delete(request); 11 | } 12 | 13 | async _fetchFromCache() { 14 | const request = new Request(this._storageKey); 15 | const cache = await self.caches.open(CACHE_NAME); 16 | const cached = await cache.match(request); 17 | return cached ? cached.text() : undefined; 18 | } 19 | 20 | async _saveToCache(data) { 21 | const request = new Request(this._storageKey); 22 | const response = new Response(data); 23 | const cache = await self.caches.open(CACHE_NAME); 24 | return cache.put(request, response); 25 | } 26 | } 27 | 28 | module.exports = CacheApiSession; 29 | -------------------------------------------------------------------------------- /src/lib/gramjs/sessions/IdbSession.js: -------------------------------------------------------------------------------- 1 | const idb = require('idb-keyval'); 2 | const StorageSession = require('./StorageSession'); 3 | 4 | const CACHE_NAME = 'GramJs'; 5 | 6 | class IdbSession extends StorageSession { 7 | _delete() { 8 | return idb.del(`${CACHE_NAME}:${this._storageKey}`); 9 | } 10 | 11 | _fetchFromCache() { 12 | return idb.get(`${CACHE_NAME}:${this._storageKey}`); 13 | } 14 | 15 | _saveToCache(data) { 16 | return idb.set(`${CACHE_NAME}:${this._storageKey}`, data); 17 | } 18 | } 19 | 20 | module.exports = IdbSession; 21 | -------------------------------------------------------------------------------- /src/lib/gramjs/sessions/LocalStorageSession.js: -------------------------------------------------------------------------------- 1 | const StorageSession = require('./StorageSession'); 2 | 3 | class LocalStorageSession extends StorageSession { 4 | _delete() { 5 | return localStorage.removeItem(this._storageKey); 6 | } 7 | 8 | _fetchFromCache() { 9 | return localStorage.getItem(this._storageKey); 10 | } 11 | 12 | _saveToCache(data) { 13 | return localStorage.setItem(this._storageKey, data); 14 | } 15 | } 16 | 17 | module.exports = LocalStorageSession; 18 | -------------------------------------------------------------------------------- /src/lib/gramjs/sessions/index.js: -------------------------------------------------------------------------------- 1 | const Memory = require('./Memory'); 2 | const StringSession = require('./StringSession'); 3 | const CacheApiSession = require('./CacheApiSession'); 4 | const LocalStorageSession = require('./LocalStorageSession'); 5 | const IdbSession = require('./IdbSession'); 6 | const CallbackSession = require('./CallbackSession'); 7 | 8 | module.exports = { 9 | Memory, 10 | StringSession, 11 | CacheApiSession, 12 | LocalStorageSession, 13 | IdbSession, 14 | CallbackSession, 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/gramjs/tl/AllTLObjects.js: -------------------------------------------------------------------------------- 1 | const api = require('./api'); 2 | 3 | const LAYER = 131; 4 | const tlobjects = {}; 5 | 6 | 7 | for (const tl of Object.values(api)) { 8 | if (tl.CONSTRUCTOR_ID) { 9 | tlobjects[tl.CONSTRUCTOR_ID] = tl; 10 | } else { 11 | for (const sub of Object.values(tl)) { 12 | tlobjects[sub.CONSTRUCTOR_ID] = sub; 13 | } 14 | } 15 | } 16 | 17 | module.exports = { 18 | LAYER, 19 | tlobjects, 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/gramjs/tl/core/TLMessage.js: -------------------------------------------------------------------------------- 1 | class TLMessage { 2 | static SIZE_OVERHEAD = 12; 3 | 4 | static classType = 'constructor'; 5 | 6 | constructor(msgId, seqNo, obj) { 7 | this.msgId = msgId; 8 | this.seqNo = seqNo; 9 | this.obj = obj; 10 | this.classType = 'constructor'; 11 | } 12 | } 13 | 14 | module.exports = TLMessage; 15 | -------------------------------------------------------------------------------- /src/lib/gramjs/tl/core/index.js: -------------------------------------------------------------------------------- 1 | const TLMessage = require('./TLMessage'); 2 | const RPCResult = require('./RPCResult'); 3 | const MessageContainer = require('./MessageContainer'); 4 | const GZIPPacked = require('./GZIPPacked'); 5 | 6 | const coreObjects = { 7 | [RPCResult.CONSTRUCTOR_ID]: RPCResult, 8 | [GZIPPacked.CONSTRUCTOR_ID]: GZIPPacked, 9 | [MessageContainer.CONSTRUCTOR_ID]: MessageContainer, 10 | }; 11 | 12 | module.exports = { 13 | TLMessage, 14 | RPCResult, 15 | MessageContainer, 16 | GZIPPacked, 17 | coreObjects, 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/gramjs/tl/index.js: -------------------------------------------------------------------------------- 1 | const api = require('./api'); 2 | const { 3 | serializeBytes, 4 | serializeDate, 5 | } = require('./generationHelpers'); 6 | 7 | module.exports = { 8 | // TODO Refactor internal usages to always use `api`. 9 | constructors: api, 10 | requests: api, 11 | serializeBytes, 12 | serializeDate, 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/rlottie/rlottie.worker.ts: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return new Worker(__webpack_public_path__ + "ee6b033138d49dc83303.worker.js"); 3 | }; -------------------------------------------------------------------------------- /src/lib/webp/webp_wasm.worker.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return new Worker(__webpack_public_path__ + "4c9029ba18603b924ede.worker.js"); 3 | }; -------------------------------------------------------------------------------- /src/modules/actions/apiUpdaters/misc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addReducer, getGlobal, setGlobal, 3 | } from '../../../lib/teact/teactn'; 4 | 5 | import { ApiUpdate } from '../../../api/types'; 6 | import { ApiPrivacyKey } from '../../../types'; 7 | 8 | import { addBlockedContact, removeBlockedContact } from '../../reducers'; 9 | 10 | addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { 11 | switch (update['@type']) { 12 | case 'updatePeerBlocked': 13 | if (update.isBlocked) { 14 | return addBlockedContact(getGlobal(), update.id); 15 | } else { 16 | return removeBlockedContact(getGlobal(), update.id); 17 | } 18 | 19 | case 'updateResetContactList': 20 | setGlobal({ 21 | ...getGlobal(), 22 | contactList: { 23 | hash: 0, 24 | userIds: [], 25 | }, 26 | }); 27 | break; 28 | 29 | case 'updateFavoriteStickers': 30 | actions.loadFavoriteStickers(); 31 | break; 32 | 33 | case 'updatePrivacy': 34 | global.settings.privacy[update.key as ApiPrivacyKey] = update.rules; 35 | break; 36 | } 37 | 38 | return undefined; 39 | }); 40 | -------------------------------------------------------------------------------- /src/modules/actions/apiUpdaters/settings.ts: -------------------------------------------------------------------------------- 1 | import { addReducer, setGlobal } from '../../../lib/teact/teactn'; 2 | 3 | import { ApiUpdate } from '../../../api/types'; 4 | import { GlobalState } from '../../../global/types'; 5 | import { addNotifyException, updateChat, updateNotifySettings } from '../../reducers'; 6 | 7 | addReducer('apiUpdate', (global, actions, update: ApiUpdate): GlobalState | undefined => { 8 | switch (update['@type']) { 9 | case 'updateNotifySettings': { 10 | return updateNotifySettings(global, update.peerType, update.isSilent, update.shouldShowPreviews); 11 | } 12 | 13 | case 'updateNotifyExceptions': { 14 | const { 15 | chatId, isMuted, isSilent, shouldShowPreviews, 16 | } = update; 17 | const chat = global.chats.byId[chatId]; 18 | 19 | if (chat) { 20 | global = updateChat(global, chatId, { isMuted }); 21 | } 22 | 23 | setGlobal(addNotifyException(global, chatId, { isMuted, isSilent, shouldShowPreviews })); 24 | break; 25 | } 26 | } 27 | 28 | return undefined; 29 | }); 30 | -------------------------------------------------------------------------------- /src/modules/actions/apiUpdaters/symbols.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | 3 | import { ApiUpdate } from '../../../api/types'; 4 | 5 | import { updateStickerSet } from '../../reducers'; 6 | 7 | addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { 8 | switch (update['@type']) { 9 | case 'updateStickerSet': { 10 | return updateStickerSet(global, update.id, update.stickerSet); 11 | } 12 | } 13 | 14 | return undefined; 15 | }); 16 | -------------------------------------------------------------------------------- /src/modules/actions/apiUpdaters/twoFaSettings.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | 3 | import { ApiUpdate } from '../../../api/types'; 4 | import { GlobalState } from '../../../global/types'; 5 | 6 | addReducer('apiUpdate', (global, actions, update: ApiUpdate): GlobalState | undefined => { 7 | switch (update['@type']) { 8 | case 'updateTwoFaStateWaitCode': { 9 | return { 10 | ...global, 11 | twoFaSettings: { 12 | ...global.twoFaSettings, 13 | isLoading: false, 14 | waitingEmailCodeLength: update.length, 15 | }, 16 | }; 17 | } 18 | 19 | case 'updateTwoFaError': { 20 | return { 21 | ...global, 22 | twoFaSettings: { 23 | ...global.twoFaSettings, 24 | error: update.message, 25 | }, 26 | }; 27 | } 28 | } 29 | 30 | return undefined; 31 | }); 32 | -------------------------------------------------------------------------------- /src/modules/actions/ui/payments.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | import { 3 | clearPayment, closeInvoice, 4 | } from '../../reducers'; 5 | 6 | addReducer('openPaymentModal', (global, actions, payload) => { 7 | const { messageId } = payload; 8 | return { 9 | ...global, 10 | payment: { 11 | ...global.payment, 12 | messageId, 13 | isPaymentModalOpen: true, 14 | }, 15 | }; 16 | }); 17 | 18 | addReducer('closePaymentModal', (global) => { 19 | const newGlobal = clearPayment(global); 20 | return closeInvoice(newGlobal); 21 | }); 22 | -------------------------------------------------------------------------------- /src/modules/actions/ui/settings.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | import { ISettings, IThemeSettings, ThemeKey } from '../../../types'; 3 | import { replaceSettings, replaceThemeSettings } from '../../reducers'; 4 | 5 | addReducer('setSettingOption', (global, actions, payload?: Partial) => { 6 | return replaceSettings(global, payload); 7 | }); 8 | 9 | addReducer('setThemeSettings', (global, actions, payload: { theme: ThemeKey } & Partial) => { 10 | const { theme, ...settings } = payload; 11 | 12 | return replaceThemeSettings(global, theme, settings); 13 | }); 14 | -------------------------------------------------------------------------------- /src/modules/actions/ui/stickerSearch.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | 3 | addReducer('setStickerSearchQuery', (global, actions, payload) => { 4 | const { query } = payload!; 5 | 6 | return { 7 | ...global, 8 | stickers: { 9 | ...global.stickers, 10 | search: { 11 | query, 12 | resultIds: undefined, 13 | }, 14 | }, 15 | }; 16 | }); 17 | 18 | addReducer('setGifSearchQuery', (global, actions, payload) => { 19 | const { query } = payload!; 20 | 21 | return { 22 | ...global, 23 | gifs: { 24 | ...global.gifs, 25 | search: { 26 | query, 27 | offsetId: undefined, 28 | results: undefined, 29 | }, 30 | }, 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /src/modules/actions/ui/users.ts: -------------------------------------------------------------------------------- 1 | import { addReducer } from '../../../lib/teact/teactn'; 2 | 3 | import { GlobalState } from '../../../global/types'; 4 | 5 | import { updateSelectedUserId, updateUserSearch } from '../../reducers'; 6 | 7 | addReducer('openUserInfo', (global, actions, payload) => { 8 | const { id } = payload!; 9 | 10 | actions.openChat({ id }); 11 | }); 12 | 13 | const clearSelectedUserId = (global: GlobalState) => updateSelectedUserId(global, undefined); 14 | 15 | addReducer('openChat', clearSelectedUserId); 16 | 17 | addReducer('setUserSearchQuery', (global, actions, payload) => { 18 | const { query } = payload!; 19 | 20 | return updateUserSearch(global, { 21 | globalUserIds: undefined, 22 | localUserIds: undefined, 23 | fetchingStatus: Boolean(query), 24 | query, 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/modules/helpers/localSearch.ts: -------------------------------------------------------------------------------- 1 | export function buildChatThreadKey(chatId: number, threadId: number) { 2 | return `${chatId}_${threadId}`; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/reducers/bots.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | import { InlineBotSettings } from '../../types'; 3 | 4 | 5 | export function replaceInlineBotSettings( 6 | global: GlobalState, username: string, inlineBotSettings: InlineBotSettings | false, 7 | ): GlobalState { 8 | return { 9 | ...global, 10 | inlineBots: { 11 | ...global.inlineBots, 12 | byUsername: { 13 | ...global.inlineBots.byUsername, 14 | [username]: inlineBotSettings, 15 | }, 16 | }, 17 | }; 18 | } 19 | 20 | export function replaceInlineBotsIsLoading(global: GlobalState, isLoading: boolean): GlobalState { 21 | return { 22 | ...global, 23 | inlineBots: { 24 | ...global.inlineBots, 25 | isLoading, 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/reducers/management.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | import { ManagementProgress } from '../../types'; 3 | 4 | interface ManagementState { 5 | isActive: boolean; 6 | isUsernameAvailable?: boolean; 7 | error?: string; 8 | } 9 | 10 | export function updateManagementProgress(global: GlobalState, progress: ManagementProgress): GlobalState { 11 | return { 12 | ...global, 13 | management: { 14 | ...global.management, 15 | progress, 16 | }, 17 | }; 18 | } 19 | 20 | export function updateManagement(global: GlobalState, chatId: number, update: Partial): GlobalState { 21 | return { 22 | ...global, 23 | management: { 24 | ...global.management, 25 | byChatId: { 26 | ...global.management.byChatId, 27 | [chatId]: { 28 | ...(global.management.byChatId[chatId] || {}), 29 | ...update, 30 | }, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/reducers/twoFaSettings.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | 3 | export function updateTwoFaSettings( 4 | global: GlobalState, 5 | update: GlobalState['twoFaSettings'], 6 | ): GlobalState { 7 | return { 8 | ...global, 9 | twoFaSettings: { 10 | ...global.twoFaSettings, 11 | ...update, 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/selectors/globalSearch.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | 3 | export function selectCurrentGlobalSearchQuery(global: GlobalState) { 4 | return global.globalSearch.query; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/selectors/payments.ts: -------------------------------------------------------------------------------- 1 | 2 | import { GlobalState } from '../../global/types'; 3 | 4 | export function selectPaymentMessageId(global: GlobalState) { 5 | return global.payment.messageId; 6 | } 7 | 8 | export function selectPaymentRequestId(global: GlobalState) { 9 | return global.payment.formId; 10 | } 11 | 12 | export function selectProviderPublishableKey(global: GlobalState) { 13 | return global.payment.nativeParams ? global.payment.nativeParams.publishableKey : undefined; 14 | } 15 | 16 | export function selectStripeCredentials(global: GlobalState) { 17 | return global.payment.stripeCredentials; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/selectors/settings.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | 3 | export function selectNotifySettings(global: GlobalState) { 4 | return global.settings.byKey; 5 | } 6 | 7 | export function selectNotifyExceptions(global: GlobalState) { 8 | return global.settings.notifyExceptions; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/selectors/users.ts: -------------------------------------------------------------------------------- 1 | import { GlobalState } from '../../global/types'; 2 | import { ApiChat, ApiUser } from '../../api/types'; 3 | 4 | export function selectUser(global: GlobalState, userId: number): ApiUser | undefined { 5 | return global.users.byId[userId]; 6 | } 7 | 8 | export function selectIsUserBlocked(global: GlobalState, userId: number) { 9 | const user = selectUser(global, userId); 10 | 11 | return user && user.fullInfo && user.fullInfo.isBlocked; 12 | } 13 | 14 | // Slow, not to be used in `withGlobal` 15 | export function selectUserByUsername(global: GlobalState, username: string) { 16 | const usernameLowered = username.toLowerCase(); 17 | return Object.values(global.users.byId).find( 18 | (user) => user.username.toLowerCase() === usernameLowered, 19 | ); 20 | } 21 | 22 | export function selectIsUserOrChatContact(global: GlobalState, userOrChat: ApiUser | ApiChat) { 23 | return global.contactList && global.contactList.userIds.includes(userOrChat.id); 24 | } 25 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | export var ServiceWorkerNoSupportError = (function() { 2 | 3 | function ServiceWorkerNoSupportError() { 4 | var err = Error.call(this, 'ServiceWorker is not supported.'); 5 | Object.setPrototypeOf(err, ServiceWorkerNoSupportError.prototype); 6 | return err; 7 | } 8 | 9 | ServiceWorkerNoSupportError.prototype = Object.create(Error.prototype); 10 | 11 | return ServiceWorkerNoSupportError; 12 | })(); 13 | 14 | export var scriptUrl = __webpack_public_path__ + "serviceWorker.js"; 15 | 16 | export default function registerServiceWorkerIfSupported(mapScriptUrlOrOptions, maybeOptions) { 17 | 18 | var targetScriptUrl = scriptUrl; 19 | var options = maybeOptions; 20 | 21 | if (typeof mapScriptUrlOrOptions === 'function') { 22 | targetScriptUrl = mapScriptUrlOrOptions(targetScriptUrl); 23 | } else { 24 | options = mapScriptUrlOrOptions; 25 | } 26 | 27 | if ('serviceWorker' in navigator) { 28 | return navigator.serviceWorker.register(targetScriptUrl, options); 29 | } 30 | 31 | return Promise.reject(new ServiceWorkerNoSupportError()); 32 | } 33 | -------------------------------------------------------------------------------- /src/serviceWorker/assetCache.ts: -------------------------------------------------------------------------------- 1 | import { ASSET_CACHE_NAME } from '../config'; 2 | 3 | declare const self: ServiceWorkerGlobalScope; 4 | 5 | export async function respondWithCache(e: FetchEvent) { 6 | const cache = await self.caches.open(ASSET_CACHE_NAME); 7 | const cached = await cache.match(e.request); 8 | 9 | if (cached) { 10 | return cached; 11 | } 12 | 13 | const remote = await fetch(e.request); 14 | cache.put(e.request, remote.clone()); 15 | 16 | return remote; 17 | } 18 | 19 | export function clearAssetCache() { 20 | return self.caches.delete(ASSET_CACHE_NAME); 21 | } 22 | -------------------------------------------------------------------------------- /src/util/activeTabMonitor.ts: -------------------------------------------------------------------------------- 1 | const STORAGE_KEY = 'tt-active-tab'; 2 | const INTERVAL = 2000; 3 | 4 | const tabKey = String(Date.now() + Math.random()); 5 | 6 | localStorage.setItem(STORAGE_KEY, tabKey); 7 | 8 | let callback: NoneToVoidFunction; 9 | 10 | const interval = window.setInterval(() => { 11 | if (callback && localStorage.getItem(STORAGE_KEY) !== tabKey) { 12 | callback(); 13 | clearInterval(interval); 14 | } 15 | }, INTERVAL); 16 | 17 | export function addActiveTabChangeListener(_callback: NoneToVoidFunction) { 18 | callback = _callback; 19 | } 20 | -------------------------------------------------------------------------------- /src/util/animation.ts: -------------------------------------------------------------------------------- 1 | import { fastRaf } from './schedulers'; 2 | 3 | interface AnimationInstance { 4 | isCancelled: boolean; 5 | } 6 | 7 | let currentInstance: AnimationInstance | undefined; 8 | 9 | export function animateSingle(tick: Function, instance?: AnimationInstance) { 10 | if (!instance) { 11 | if (currentInstance && !currentInstance.isCancelled) { 12 | currentInstance.isCancelled = true; 13 | } 14 | 15 | instance = { isCancelled: false }; 16 | currentInstance = instance; 17 | } 18 | 19 | fastRaf(() => { 20 | if (!instance!.isCancelled && tick()) { 21 | animateSingle(tick, instance); 22 | } 23 | }); 24 | } 25 | 26 | export function animate(tick: Function) { 27 | fastRaf(() => { 28 | if (tick()) { 29 | animate(tick); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/util/appBadge.ts: -------------------------------------------------------------------------------- 1 | import { DEBUG } from '../config'; 2 | 3 | export function updateAppBadge(unreadCount: number) { 4 | if (typeof window.navigator.setAppBadge !== 'function') { 5 | return; 6 | } 7 | 8 | window.navigator.setAppBadge(unreadCount).catch((err) => { 9 | if (DEBUG) { 10 | // eslint-disable-next-line no-console 11 | console.error(err); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/util/buildClassName.ts: -------------------------------------------------------------------------------- 1 | type Parts = (string | false | undefined)[]; 2 | 3 | export default (...parts: Parts) => { 4 | return parts.filter(Boolean).join(' '); 5 | }; 6 | -------------------------------------------------------------------------------- /src/util/callbacks.ts: -------------------------------------------------------------------------------- 1 | export function createCallbackManager() { 2 | const callbacks: AnyToVoidFunction[] = []; 3 | 4 | function addCallback(cb: AnyToVoidFunction) { 5 | callbacks.push(cb); 6 | 7 | return () => { 8 | removeCallback(cb); 9 | }; 10 | } 11 | 12 | function removeCallback(cb: AnyToVoidFunction) { 13 | const index = callbacks.indexOf(cb); 14 | if (index !== -1) { 15 | callbacks.splice(index, 1); 16 | } 17 | } 18 | 19 | function runCallbacks(...args: any[]) { 20 | callbacks.forEach((callback) => { 21 | callback(...args); 22 | }); 23 | } 24 | 25 | return { 26 | runCallbacks, 27 | addCallback, 28 | removeCallback, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/util/captureEscKeyListener.ts: -------------------------------------------------------------------------------- 1 | import captureKeyboardListener from './captureKeyboardListeners'; 2 | 3 | type IHandlerFunction = () => void; 4 | 5 | export default function captureEscKeyListener(handler: IHandlerFunction) { 6 | return captureKeyboardListener({ onEsc: handler }); 7 | } 8 | -------------------------------------------------------------------------------- /src/util/cycleRestrict.ts: -------------------------------------------------------------------------------- 1 | export default function cycleRestrict(length: number, index: number) { 2 | return index - Math.floor(index / length) * length; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/deeplink.ts: -------------------------------------------------------------------------------- 1 | import { getDispatch } from '../lib/teact/teactn'; 2 | 3 | export const processDeepLink = (url: string) => { 4 | const { protocol, searchParams, pathname } = new URL(url); 5 | 6 | if (protocol !== 'tg:') return; 7 | 8 | const { openChatByUsername, openStickerSetShortName } = getDispatch(); 9 | 10 | const method = pathname.replace(/^\/\//, ''); 11 | const params: Record = {}; 12 | searchParams.forEach((value, key) => { 13 | params[key] = value; 14 | }); 15 | 16 | switch (method) { 17 | case 'resolve': { 18 | const { 19 | domain, 20 | } = params; 21 | 22 | if (domain !== 'telegrampassport') { 23 | openChatByUsername({ 24 | username: domain, 25 | }); 26 | } 27 | break; 28 | } 29 | case 'privatepost': 30 | 31 | break; 32 | case 'bg': 33 | 34 | break; 35 | case 'join': 36 | 37 | break; 38 | case 'addstickers': { 39 | const { set } = params; 40 | 41 | openStickerSetShortName({ 42 | stickerSetShortName: set, 43 | }); 44 | break; 45 | } 46 | case 'msg': 47 | 48 | break; 49 | default: 50 | // Unsupported deeplink 51 | 52 | break; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/util/deleteLastCharacterOutsideSelection.ts: -------------------------------------------------------------------------------- 1 | export default function deleteLastCharacterOutsideSelection(html: string) { 2 | const tempInput = document.createElement('div'); 3 | tempInput.contentEditable = 'true'; 4 | tempInput.style.position = 'absolute'; 5 | tempInput.style.left = '-10000px'; 6 | tempInput.style.top = '-10000px'; 7 | tempInput.innerHTML = html; 8 | document.body.appendChild(tempInput); 9 | let element = tempInput.lastChild!; 10 | 11 | if (element.lastChild) { 12 | // Selects the last and the deepest child of the element. 13 | while (element.lastChild) { 14 | element = element.lastChild; 15 | } 16 | } 17 | 18 | // Gets length of the element's content. 19 | const textLength = element.textContent!.length; 20 | const range = document.createRange(); 21 | const selection = window.getSelection()!; 22 | 23 | // Sets selection position to the end of the element. 24 | range.setStart(element, textLength); 25 | range.setEnd(element, textLength); 26 | selection.removeAllRanges(); 27 | selection.addRange(range); 28 | document.execCommand('delete', false); 29 | 30 | const result = tempInput.innerHTML; 31 | document.body.removeChild(tempInput); 32 | 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /src/util/download.ts: -------------------------------------------------------------------------------- 1 | export default function download(url: string, filename: string) { 2 | const link = document.createElement('a'); 3 | link.href = url; 4 | link.download = filename; 5 | link.click(); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/environmentSystemTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeKey } from '../types'; 2 | 3 | let systemThemeCache: ThemeKey = ( 4 | window && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 5 | ) ? 'dark' : 'light'; 6 | 7 | export function getSystemTheme() { 8 | return systemThemeCache; 9 | } 10 | 11 | function handleSystemThemeChange(e: MediaQueryListEventMap['change']) { 12 | systemThemeCache = e.matches ? 'dark' : 'light'; 13 | } 14 | 15 | const mql = window.matchMedia('(prefers-color-scheme: dark)'); 16 | if (typeof mql.addEventListener === 'function') { 17 | mql.addEventListener('change', handleSystemThemeChange); 18 | } else if (typeof mql.addListener === 'function') { 19 | mql.addListener(handleSystemThemeChange); 20 | } 21 | -------------------------------------------------------------------------------- /src/util/environmentWebp.ts: -------------------------------------------------------------------------------- 1 | let isWebpSupportedCache: boolean | undefined; 2 | 3 | export function isWebpSupported() { 4 | return Boolean(isWebpSupportedCache); 5 | } 6 | 7 | function testWebp(): Promise { 8 | return new Promise((resolve) => { 9 | const webp = new Image(); 10 | // eslint-disable-next-line max-len 11 | webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'; 12 | const handleLoadOrError = () => { 13 | resolve(webp.height === 2); 14 | }; 15 | webp.onload = handleLoadOrError; 16 | webp.onerror = handleLoadOrError; 17 | }); 18 | } 19 | 20 | testWebp().then((hasWebp) => { 21 | isWebpSupportedCache = hasWebp; 22 | }); 23 | -------------------------------------------------------------------------------- /src/util/focusEditableElement.ts: -------------------------------------------------------------------------------- 1 | import { IS_TOUCH_ENV } from './environment'; 2 | 3 | export default function focusEditableElement(element: HTMLElement, force?: boolean) { 4 | if (!force && element === document.activeElement) { 5 | return; 6 | } 7 | 8 | const selection = window.getSelection()!; 9 | const range = document.createRange(); 10 | const lastChild = element.lastChild || element; 11 | 12 | if (!IS_TOUCH_ENV && (!lastChild || !lastChild.nodeValue)) { 13 | element.focus(); 14 | return; 15 | } 16 | 17 | range.selectNodeContents(lastChild); 18 | // `false` means collapse to the end rather than the start 19 | range.collapse(false); 20 | selection.removeAllRanges(); 21 | selection.addRange(range); 22 | } 23 | -------------------------------------------------------------------------------- /src/util/fonts.ts: -------------------------------------------------------------------------------- 1 | const SITE_FONTS = ['400 1em Roboto', '500 1em Roboto']; 2 | 3 | export default function preloadFonts() { 4 | if ('fonts' in document) { 5 | return Promise.all(SITE_FONTS.map((font) => document.fonts.load(font))); 6 | } 7 | 8 | return undefined; 9 | } 10 | -------------------------------------------------------------------------------- /src/util/generateIdFor.ts: -------------------------------------------------------------------------------- 1 | export default (store: AnyLiteral) => { 2 | let id; 3 | 4 | do { 5 | id = String(Math.random()).replace('0.', 'id'); 6 | } while (store.hasOwnProperty(id)); 7 | 8 | return id; 9 | }; 10 | -------------------------------------------------------------------------------- /src/util/handleError.ts: -------------------------------------------------------------------------------- 1 | import { DEBUG_ALERT_MSG } from '../config'; 2 | import { throttle } from './schedulers'; 3 | 4 | window.addEventListener('error', handleErrorEvent); 5 | window.addEventListener('unhandledrejection', handleErrorEvent); 6 | 7 | // eslint-disable-next-line prefer-destructuring 8 | const APP_ENV = process.env.APP_ENV; 9 | 10 | function handleErrorEvent(e: ErrorEvent | PromiseRejectionEvent) { 11 | e.preventDefault(); 12 | 13 | handleError(e instanceof ErrorEvent ? e.error : e.reason); 14 | } 15 | 16 | const throttledAlert = throttle(window.alert, 1000); 17 | 18 | export function handleError(err: Error) { 19 | // eslint-disable-next-line no-console 20 | console.error(err); 21 | 22 | if (APP_ENV === 'development' || APP_ENV === 'staging') { 23 | throttledAlert(`${DEBUG_ALERT_MSG}\n\n${(err && err.message) || err}\n${err && err.stack}`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/util/insertHtmlInSelection.ts: -------------------------------------------------------------------------------- 1 | export default function insertHtmlInSelection(html: string) { 2 | const selection = window.getSelection(); 3 | 4 | if (selection && selection.getRangeAt && selection.rangeCount) { 5 | const range = selection.getRangeAt(0); 6 | range.deleteContents(); 7 | 8 | const fragment = range.createContextualFragment(html); 9 | const lastInsertedNode = fragment.lastChild; 10 | range.insertNode(fragment); 11 | if (lastInsertedNode) { 12 | range.setStartAfter(lastInsertedNode); 13 | range.setEndAfter(lastInsertedNode); 14 | } else { 15 | range.collapse(false); 16 | } 17 | selection.removeAllRanges(); 18 | selection.addRange(range); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/util/isFullyVisible.ts: -------------------------------------------------------------------------------- 1 | function isFullyVisible(container: HTMLElement, element: HTMLElement, isHorizontal = false) { 2 | const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop']; 3 | const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight']; 4 | const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop']; 5 | const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight']; 6 | return y1 > viewportY1 && y2 < viewportY2; 7 | } 8 | 9 | export default isFullyVisible; 10 | -------------------------------------------------------------------------------- /src/util/memo.ts: -------------------------------------------------------------------------------- 1 | export const MEMO_EMPTY_ARRAY = []; 2 | -------------------------------------------------------------------------------- /src/util/patchSafariProgressiveAudio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Thanks to Ace Monkey for this mind-blowing patch. 3 | */ 4 | 5 | export function patchSafariProgressiveAudio(audioEl: HTMLAudioElement) { 6 | if (audioEl.dataset.patchedForSafari) { 7 | return; 8 | } 9 | 10 | audioEl.addEventListener('play', () => { 11 | const t = audioEl.currentTime; 12 | 13 | function onProgress() { 14 | if (!audioEl.buffered.length) { 15 | return; 16 | } 17 | 18 | audioEl.dataset.patchForSafariInProgress = 'true'; 19 | audioEl.currentTime = audioEl.duration - 1; 20 | audioEl.addEventListener('progress', () => { 21 | delete audioEl.dataset.patchForSafariInProgress; 22 | audioEl.currentTime = t; 23 | if (audioEl.paused) { 24 | audioEl.play(); 25 | } 26 | }, { once: true }); 27 | 28 | audioEl.removeEventListener('progress', onProgress); 29 | } 30 | 31 | audioEl.addEventListener('progress', onProgress); 32 | }, { once: true }); 33 | 34 | audioEl.dataset.patchedForSafari = 'true'; 35 | } 36 | 37 | export function isSafariPatchInProgress(audioEl: HTMLAudioElement) { 38 | return Boolean(audioEl.dataset.patchForSafariInProgress); 39 | } 40 | -------------------------------------------------------------------------------- /src/util/requestQuery.ts: -------------------------------------------------------------------------------- 1 | export function buildQueryString(data: Record) { 2 | const query = Object.keys(data).map((k) => `${k}=${data[k]}`).join('&'); 3 | return query.length > 0 ? `?${query}` : ''; 4 | } 5 | -------------------------------------------------------------------------------- /src/util/resetScroll.ts: -------------------------------------------------------------------------------- 1 | import { IS_IOS } from './environment'; 2 | 3 | export default (container: HTMLDivElement, scrollTop?: number) => { 4 | if (IS_IOS) { 5 | container.style.overflow = 'hidden'; 6 | } 7 | 8 | if (scrollTop !== undefined) { 9 | container.scrollTop = scrollTop; 10 | } 11 | 12 | if (IS_IOS) { 13 | container.style.overflow = ''; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/routing.ts: -------------------------------------------------------------------------------- 1 | import { MessageList, MessageListType } from '../global/types'; 2 | import { MAIN_THREAD_ID } from '../api/types'; 3 | 4 | export const createMessageHash = (messageList: MessageList): string => ( 5 | messageList.chatId.toString() 6 | + (messageList.type !== 'thread' ? `_${messageList.type}` 7 | : (messageList.threadId !== -1 ? `_${messageList.threadId}` : '')) 8 | ); 9 | 10 | export const parseMessageHash = (value: string): MessageList => { 11 | const [chatId, typeOrThreadId] = value.split('_'); 12 | const isType = ['thread', 'pinned', 'scheduled'].includes(typeOrThreadId); 13 | 14 | return { 15 | chatId: Number(chatId), 16 | type: !!typeOrThreadId && isType ? (typeOrThreadId as MessageListType) : 'thread', 17 | threadId: !!typeOrThreadId && !isType ? Number(typeOrThreadId) : MAIN_THREAD_ID, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/util/safePlay.ts: -------------------------------------------------------------------------------- 1 | import { DEBUG } from '../config'; 2 | 3 | export default (mediaEl: HTMLMediaElement) => { 4 | mediaEl.play().catch((err) => { 5 | if (DEBUG) { 6 | // eslint-disable-next-line no-console 7 | console.warn(err); 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/searchWords.ts: -------------------------------------------------------------------------------- 1 | let RE_NOT_LETTER: RegExp; 2 | 3 | try { 4 | RE_NOT_LETTER = new RegExp('[^\\p{L}\\p{M}]+', 'ui'); 5 | } catch (e) { 6 | // Support for older versions of firefox 7 | RE_NOT_LETTER = new RegExp('[^\\wа-яё]+', 'i'); 8 | } 9 | 10 | export default function searchWords(haystack: string, needle: string) { 11 | if (!haystack || !needle) { 12 | return false; 13 | } 14 | 15 | const haystackWords = haystack.toLowerCase().split(RE_NOT_LETTER); 16 | const needleWords = needle.toLowerCase().split(RE_NOT_LETTER); 17 | 18 | return needleWords.every((needleWord) => ( 19 | haystackWords.some((haystackWord) => haystackWord.startsWith(needleWord)) 20 | )); 21 | } 22 | -------------------------------------------------------------------------------- /src/util/setTooltipItemVisible.ts: -------------------------------------------------------------------------------- 1 | import findInViewport from './findInViewport'; 2 | import isFullyVisible from './isFullyVisible'; 3 | import fastSmoothScroll from './fastSmoothScroll'; 4 | 5 | const VIEWPORT_MARGIN = 8; 6 | const SCROLL_MARGIN = 10; 7 | 8 | export default function setTooltipItemVisible(selector: string, index: number, containerRef: Record) { 9 | const container = containerRef.current!; 10 | if (!container || index < 0) { 11 | return; 12 | } 13 | const { visibleIndexes, allElements } = findInViewport( 14 | container, 15 | selector, 16 | VIEWPORT_MARGIN, 17 | true, 18 | true, 19 | ); 20 | 21 | if (!allElements.length || !allElements[index]) { 22 | return; 23 | } 24 | const first = visibleIndexes[0]; 25 | if (!visibleIndexes.includes(index) 26 | || (index === first && !isFullyVisible(container, allElements[first]))) { 27 | const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end'; 28 | fastSmoothScroll(container, allElements[index], position, SCROLL_MARGIN); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/util/systemFilesDialog.ts: -------------------------------------------------------------------------------- 1 | let fileSelector: HTMLInputElement; 2 | 3 | export function openSystemFilesDialog(accept = '*', callback: (e: Event) => void, noMultiple = false) { 4 | if (!fileSelector) { 5 | fileSelector = document.createElement('input'); 6 | fileSelector.setAttribute('type', 'file'); 7 | } 8 | 9 | fileSelector.setAttribute('accept', accept); 10 | 11 | if (noMultiple) { 12 | fileSelector.removeAttribute('multiple'); 13 | } else { 14 | fileSelector.setAttribute('multiple', 'multiple'); 15 | } 16 | 17 | // eslint-disable-next-line no-null/no-null 18 | fileSelector.onchange = null; 19 | fileSelector.value = ''; 20 | fileSelector.onchange = callback; 21 | 22 | fileSelector.click(); 23 | } 24 | -------------------------------------------------------------------------------- /src/util/textFormat.ts: -------------------------------------------------------------------------------- 1 | export function formatInteger(value: number) { 2 | return String(value).replace(/\d(?=(\d{3})+$)/g, '$& '); 3 | } 4 | 5 | function formatFixedNumber(number: number) { 6 | const fixed = String(number.toFixed(1)); 7 | if (fixed.substr(-2) === '.0') { 8 | return Math.round(number); 9 | } 10 | 11 | return number.toFixed(1).replace('.', ','); 12 | } 13 | 14 | export function formatIntegerCompact(views: number) { 15 | if (views < 1e3) { 16 | return views; 17 | } 18 | 19 | if (views < 1e6) { 20 | return `${formatFixedNumber(views / 1e3)}K`; 21 | } 22 | 23 | return `${formatFixedNumber(views / 1e6)}M`; 24 | } 25 | 26 | export function getFirstLetters(phrase: string, count = 2) { 27 | return phrase 28 | .replace(/[.,!@#$%^&*()_+=\-`~[\]/\\{}:"|<>?]+/gi, '') 29 | .trim() 30 | .split(/\s+/) 31 | .slice(0, count) 32 | .map((word: string) => word.length && word.match(/./u)![0].toUpperCase()) 33 | .join(''); 34 | } 35 | -------------------------------------------------------------------------------- /src/util/trimText.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_MAX_TEXT_LENGTH = 30; 2 | 3 | export default function trimText(text: string | undefined, length = DEFAULT_MAX_TEXT_LENGTH) { 4 | if (!text || text.length <= length) { 5 | return text; 6 | } 7 | 8 | return `${text.substr(0, length)}...`; 9 | } 10 | --------------------------------------------------------------------------------