From b5e85a1de8e6f03d854728b3e48644946cbb6a2c Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Wed, 4 Jan 2023 16:44:40 +0000 Subject: [PATCH] "Nicegram plus" release 1.1.8 --- Nicegram/NGAppCache/Sources/AppCache.swift | 3 + .../Presentation/Banners/BannerHelpers.swift | 21 - .../Sources/NicegramSettingsController.swift | 5 +- Nicegram/NGUtils/Sources/MediaResources.swift | 14 +- Nicegram/NGWebUtils/Sources/NGWebUtils.swift | 2 + Random.txt | 2 +- Telegram/BUILD | 9 +- .../Sources/NotificationService.swift | 15 + .../Telegram-iOS/en.lproj/Localizable.strings | 158 + .../Sources/AccountContext.swift | 11 +- .../Sources/FetchMediaUtils.swift | 8 +- .../Sources/PeerSelectionController.swift | 4 +- .../AccountUtils/Sources/AccountUtils.swift | 7 +- .../AnimationUI/Sources/AnimationNode.swift | 20 +- submodules/AppLock/Sources/AppLock.swift | 14 +- .../AppLockState/Sources/AppLockState.swift | 4 +- .../AttachmentTextInputPanelNode.swift | 2 +- .../Sources/AttachmentContainer.swift | 8 +- .../Sources/AttachmentController.swift | 4 +- .../Sources/AttachmentPanel.swift | 4 +- ...orizationSequenceCodeEntryController.swift | 6 +- ...ationSequenceCodeEntryControllerNode.swift | 66 +- .../AvatarNode/Sources/PeerAvatar.swift | 18 +- .../Sources/BotCheckoutControllerNode.swift | 8 +- .../Sources/BotCheckoutHeaderItem.swift | 14 +- .../Sources/BotReceiptControllerNode.swift | 8 +- .../BrowserUI/Sources/BrowserScreen.swift | 2 +- .../Sources/CallListController.swift | 2 +- .../Sources/ChatImportActivityScreen.swift | 2 +- .../ChatListUI/Sources/ChatContextMenus.swift | 204 +- .../Sources/ChatListController.swift | 44 - .../ChatListFilterTabContainerNode.swift | 4 +- .../Sources/ChatListSearchContainerNode.swift | 6 +- .../Sources/ChatListSearchListPaneNode.swift | 4 +- .../Sources/ChatListSearchMediaNode.swift | 4 +- .../Sources/Node/ChatListItem.swift | 50 +- .../Sources/Node/ChatListNode.swift | 12 +- .../ChatPresentationInterfaceState/BUILD | 1 + .../ChatMediaInputNodeInteraction.swift | 61 + submodules/CheckNode/Sources/CheckNode.swift | 13 + .../CodeInputView/Sources/CodeInputView.swift | 13 + submodules/ComponentFlow/BUILD | 1 + .../Base/ChildComponentTransitions.swift | 22 +- .../Source/Base/Transition.swift | 227 +- .../Source/Components/Button.swift | 33 +- .../Source/Host/ComponentHostView.swift | 40 +- .../Sources/LottieAnimationComponent.swift | 29 +- submodules/Components/PagerComponent/BUILD | 1 + .../Sources/PagerComponent.swift | 51 +- .../Sources/ReactionButtonListComponent.swift | 1 + .../Sources/ReactionImageComponent.swift | 14 +- .../ReactionListContextMenuContent.swift | 2 + submodules/Components/SheetComponent/BUILD | 1 + .../Sources/SheetComponent.swift | 109 +- .../Sources/SolidRoundedButtonComponent.swift | 17 + .../Sources/ViewControllerComponent.swift | 6 + .../Sources/ContactAddItem.swift | 7 +- .../Sources/ContactsSearchContainerNode.swift | 2 +- submodules/ContextUI/BUILD | 1 + .../ContextUI/Sources/ContextActionNode.swift | 6 +- .../Sources/ContextActionsContainerNode.swift | 2 +- .../ContextUI/Sources/ContextController.swift | 7 +- .../ContextControllerActionsStackNode.swift | 46 +- ...tControllerExtractedPresentationNode.swift | 12 +- .../Sources/DebugController.swift | 106 +- .../Sources/DirectMediaImageCache.swift | 96 +- .../DirectionalPanGestureRecognizer.swift | 2 +- .../Display/Source/CAAnimationUtils.swift | 152 +- .../Display/Source/ContextGesture.swift | 7 + .../Display/Source/DisplayLinkAnimator.swift | 12 +- submodules/Display/Source/TextNode.swift | 20 +- submodules/DrawingUI/BUILD | 99 + .../DrawingUI/MetalResources/Drawing.metal | 63 + submodules/DrawingUI/Resources/marker.png | Bin 0 -> 2538 bytes .../DrawingUI/Resources/shape_arrow.json | 1 + .../DrawingUI/Resources/shape_circle.json | 1 + .../DrawingUI/Resources/shape_rectangle.json | 1 + .../DrawingUI/Resources/shape_star.json | 1 + .../DrawingUI/Sources/ColorPickerScreen.swift | 2477 ++++++++++++++ .../DrawingUI/Sources/ConcaveHull.swift | 308 ++ .../Sources/DrawingBubbleEntity.swift | 511 +++ .../Sources/DrawingEntitiesView.swift | 781 +++++ .../DrawingUI/Sources/DrawingGesture.swift | 93 + .../DrawingUI/Sources/DrawingMetalView.swift | 713 ++++ .../DrawingUI/Sources/DrawingNeonTool.swift | 263 ++ .../DrawingUI/Sources/DrawingPenTool.swift | 745 ++++ .../DrawingUI/Sources/DrawingScreen.swift | 2996 +++++++++++++++++ .../Sources/DrawingSimpleShapeEntity.swift | 562 ++++ .../Sources/DrawingStickerEntity.swift | 759 +++++ .../DrawingUI/Sources/DrawingTextEntity.swift | 1306 +++++++ .../DrawingUI/Sources/DrawingTools.swift | 150 + .../DrawingUI/Sources/DrawingUtils.swift | 676 ++++ .../Sources/DrawingVectorEntity.swift | 403 +++ .../DrawingUI/Sources/DrawingView.swift | 1070 ++++++ .../DrawingUI/Sources/EyedropperView.swift | 243 ++ .../Sources/ModeAndSizeComponent.swift | 266 ++ .../Sources/StickerPickerScreen.swift | 1038 ++++++ .../Sources/TextSettingsComponent.swift | 770 +++++ .../DrawingUI/Sources/ToolsComponent.swift | 534 +++ submodules/DrawingUI/Sources/Unistroke.swift | 241 ++ submodules/FeaturedStickersScreen/BUILD | 39 + .../Sources/ChatMediaInputPane.swift | 19 + .../Sources/ChatMediaInputTrendingPane.swift | 63 +- .../Sources/FeaturedStickersScreen.swift | 17 +- .../Sources/MediaInputPaneTrendingItem.swift | 10 +- .../PaneSearchBarPlaceholderItem.swift | 40 +- .../Sources/StickerPaneSearchGlobaltem.swift | 78 +- .../StickerPaneSearchStickerItem.swift | 55 +- .../Sources/FetchManagerImpl.swift | 67 +- .../GalleryData/Sources/GalleryData.swift | 21 +- .../GalleryUI/Sources/GalleryController.swift | 14 +- .../Items/ChatAnimationGalleryItem.swift | 2 +- .../Items/ChatDocumentGalleryItem.swift | 2 +- .../Items/ChatExternalFileGalleryItem.swift | 2 +- .../Sources/Items/ChatImageGalleryItem.swift | 34 +- .../Items/UniversalVideoGalleryItem.swift | 49 +- .../Sources/InstantImageGalleryItem.swift | 30 +- .../Sources/InstantPageAnchorItem.swift | 2 +- .../Sources/InstantPageArticleItem.swift | 10 +- .../Sources/InstantPageArticleNode.swift | 4 +- .../Sources/InstantPageAudioItem.swift | 2 +- .../Sources/InstantPageContentNode.swift | 8 +- .../Sources/InstantPageController.swift | 18 +- .../Sources/InstantPageControllerNode.swift | 20 +- .../Sources/InstantPageDetailsItem.swift | 4 +- .../Sources/InstantPageDetailsNode.swift | 4 +- .../Sources/InstantPageFeedbackItem.swift | 2 +- .../InstantPageGalleryController.swift | 24 +- .../Sources/InstantPageImageItem.swift | 4 +- .../Sources/InstantPageImageNode.swift | 22 +- .../Sources/InstantPageItem.swift | 2 +- .../Sources/InstantPageLayout.swift | 18 +- .../InstantPagePeerReferenceItem.swift | 2 +- .../InstantPagePlayableVideoItem.swift | 4 +- .../InstantPagePlayableVideoNode.swift | 8 +- .../InstantPageReferenceController.swift | 8 +- .../InstantPageReferenceControllerNode.swift | 8 +- .../Sources/InstantPageShapeItem.swift | 2 +- .../Sources/InstantPageSlideshowItem.swift | 4 +- .../InstantPageSlideshowItemNode.swift | 12 +- .../Sources/InstantPageSubContentNode.swift | 8 +- .../Sources/InstantPageTableItem.swift | 4 +- .../Sources/InstantPageTextItem.swift | 6 +- .../Sources/InstantPageWebEmbedItem.swift | 2 +- .../Sources/InvisibleInkDustNode.swift | 66 +- .../Sources/MediaDustNode.swift | 288 +- .../Sources/InviteRequestsController.swift | 2 +- .../Sources/ItemListAvatarAndNameItem.swift | 8 +- .../Sources/ItemListPeerActionItem.swift | 21 +- .../Sources/ItemListStickerPackItem.swift | 4 +- submodules/ItemListUI/BUILD | 2 + .../Items/ItemListDisclosureItem.swift | 137 +- .../Sources/Items/ItemListSwitchItem.swift | 39 +- .../LegacyComponents/LegacyComponents.h | 5 - .../LegacyComponentsContext.h | 4 + .../TGAttachmentCarouselItemView.h | 5 +- .../TGMediaAssetsController.h | 12 +- .../LegacyComponents/TGMediaAvatarMenuMixin.h | 5 +- .../LegacyComponents/TGMediaEditingContext.h | 8 +- .../TGMediaPickerController.h | 4 + .../TGMediaPickerGalleryVideoItemView.h | 4 +- .../LegacyComponents/TGPaintUndoManager.h | 21 - .../LegacyComponents/TGPaintingData.h | 16 +- .../LegacyComponents/TGPhotoAvatarCropView.h | 4 +- .../TGPhotoEditorController.h | 14 +- .../TGPhotoEditorTabController.h | 6 + .../LegacyComponents/TGPhotoPaintEntityView.h | 1 - .../TGPhotoPaintStickersContext.h | 110 +- .../LegacyComponents/TGPhotoPaintTextEntity.h | 7 + .../LegacyComponents/TGPhotoToolbarView.h | 2 + .../LegacyComponents/TGPhotoVideoEditor.h | 2 +- .../Sources/PGPhotoCustomFilterPass.m | 5 - .../Sources/TGAttachmentCarouselItemView.m | 14 +- .../Sources/TGCameraController.m | 17 +- .../Sources/TGMediaAssetsController.m | 70 +- .../Sources/TGMediaAssetsPickerController.m | 18 +- .../Sources/TGMediaAvatarMenuMixin.m | 174 +- .../Sources/TGMediaEditingContext.m | 109 +- .../Sources/TGMediaPickerGalleryModel.m | 10 +- .../TGMediaPickerGalleryPhotoItemView.m | 35 +- .../TGMediaPickerGalleryVideoItemView.m | 50 +- .../Sources/TGMenuSheetTitleItemView.m | 8 +- .../Sources/TGPaintArrowBrush.h | 5 - .../Sources/TGPaintArrowBrush.m | 90 - .../LegacyComponents/Sources/TGPaintBrush.h | 25 - .../LegacyComponents/Sources/TGPaintBrush.m | 91 - .../Sources/TGPaintBrushPreview.h | 11 - .../Sources/TGPaintBrushPreview.m | 366 -- .../LegacyComponents/Sources/TGPaintBuffers.h | 20 - .../LegacyComponents/Sources/TGPaintBuffers.m | 102 - .../LegacyComponents/Sources/TGPaintCanvas.h | 33 - .../LegacyComponents/Sources/TGPaintCanvas.m | 335 -- .../Sources/TGPaintEllipticalBrush.h | 5 - .../Sources/TGPaintEllipticalBrush.m | 94 - .../Sources/TGPaintFaceDebugView.h | 7 - .../Sources/TGPaintFaceDebugView.m | 65 - .../LegacyComponents/Sources/TGPaintInput.h | 16 - .../LegacyComponents/Sources/TGPaintInput.m | 225 -- .../Sources/TGPaintNeonBrush.h | 5 - .../Sources/TGPaintNeonBrush.m | 113 - .../Sources/TGPaintPanGestureRecognizer.h | 8 - .../Sources/TGPaintPanGestureRecognizer.m | 38 - .../LegacyComponents/Sources/TGPaintPath.h | 53 - .../LegacyComponents/Sources/TGPaintPath.m | 130 - .../Sources/TGPaintRadialBrush.h | 5 - .../Sources/TGPaintRadialBrush.m | 85 - .../LegacyComponents/Sources/TGPaintRender.h | 16 - .../LegacyComponents/Sources/TGPaintRender.m | 325 -- .../Sources/TGPaintShaderSet.h | 8 - .../Sources/TGPaintShaderSet.m | 124 - .../LegacyComponents/Sources/TGPaintSlice.h | 15 - .../LegacyComponents/Sources/TGPaintSlice.m | 78 - .../LegacyComponents/Sources/TGPaintState.h | 13 - .../LegacyComponents/Sources/TGPaintState.m | 5 - .../LegacyComponents/Sources/TGPaintSwatch.h | 12 - .../LegacyComponents/Sources/TGPaintSwatch.m | 27 - .../LegacyComponents/Sources/TGPaintTexture.h | 14 - .../LegacyComponents/Sources/TGPaintTexture.m | 110 - .../Sources/TGPaintUndoManager.m | 158 - .../LegacyComponents/Sources/TGPainting.h | 50 - .../LegacyComponents/Sources/TGPainting.m | 723 ---- .../LegacyComponents/Sources/TGPaintingData.m | 68 +- .../Sources/TGPhotoAvatarCropView.m | 5 +- .../Sources/TGPhotoAvatarPreviewController.h | 10 +- .../Sources/TGPhotoAvatarPreviewController.m | 123 +- .../Sources/TGPhotoBrushSettingsView.h | 15 - .../Sources/TGPhotoBrushSettingsView.m | 205 -- .../Sources/TGPhotoCaptionInputMixin.m | 6 +- ...ontroller.h => TGPhotoDrawingController.h} | 7 +- .../Sources/TGPhotoDrawingController.m | 939 ++++++ .../Sources/TGPhotoEditorController.m | 253 +- .../Sources/TGPhotoEntitiesContainerView.h | 38 - .../Sources/TGPhotoEntitiesContainerView.m | 453 --- .../Sources/TGPhotoPaintActionsView.h | 15 - .../Sources/TGPhotoPaintActionsView.m | 115 - .../Sources/TGPhotoPaintColorPicker.h | 14 - .../Sources/TGPhotoPaintColorPicker.m | 737 ---- .../Sources/TGPhotoPaintController.m | 2634 --------------- .../Sources/TGPhotoPaintEntity.m | 27 - .../Sources/TGPhotoPaintEntityView.m | 247 -- .../Sources/TGPhotoPaintEyedropperView.h | 13 - .../Sources/TGPhotoPaintEyedropperView.m | 157 - .../Sources/TGPhotoPaintFont.h | 16 - .../Sources/TGPhotoPaintFont.m | 36 - .../Sources/TGPhotoPaintScrollView.h | 5 - .../Sources/TGPhotoPaintScrollView.m | 15 - .../TGPhotoPaintSelectionContainerView.h | 5 - .../TGPhotoPaintSelectionContainerView.m | 20 - .../Sources/TGPhotoPaintSettingsView.h | 47 - .../Sources/TGPhotoPaintSettingsView.m | 239 -- .../Sources/TGPhotoPaintSettingsWrapperView.h | 8 - .../Sources/TGPhotoPaintSettingsWrapperView.m | 24 - .../Sources/TGPhotoPaintStickerEntity.m | 65 - .../Sources/TGPhotoPaintTextEntity.m | 52 - .../Sources/TGPhotoStickerEntityView.h | 33 - .../Sources/TGPhotoStickerEntityView.m | 403 --- .../Sources/TGPhotoTextEntityView.h | 41 - .../Sources/TGPhotoTextEntityView.m | 851 ----- .../Sources/TGPhotoTextSettingsView.h | 13 - .../Sources/TGPhotoTextSettingsView.m | 217 -- .../Sources/TGPhotoToolbarView.m | 76 + .../Sources/TGPhotoToolsController.h | 4 +- .../Sources/TGPhotoToolsController.m | 7 +- .../Sources/TGPhotoVideoEditor.m | 72 +- .../Sources/TGVideoEditAdjustments.m | 68 +- submodules/LegacyMediaPickerUI/BUILD | 2 + .../Sources/LegacyAttachmentMenu.swift | 22 +- .../Sources/LegacyAvatarPicker.swift | 75 +- .../Sources/LegacyMediaPickers.swift | 56 +- .../Sources/LegacyPaintStickerView.swift | 187 - .../Sources/LegacyPaintStickersContext.swift | 327 +- .../LegacyUI/Sources/LegacyController.swift | 20 + .../Sources/ListMessageFileItemNode.swift | 6 +- .../Sources/ListMessageSnippetItemNode.swift | 4 +- .../LocalizedPeerData/Sources/PeerTitle.swift | 4 +- submodules/MediaPickerUI/BUILD | 1 + .../Sources/LegacyMediaPickerGallery.swift | 13 +- .../Sources/MediaPickerGridItem.swift | 167 +- .../Sources/MediaPickerScreen.swift | 71 +- .../Sources/MediaPickerSelectedListNode.swift | 170 +- submodules/MediaPlayer/BUILD | 1 + .../Sources/FFMpegMediaFrameSource.swift | 12 +- .../FFMpegMediaFrameSourceContext.swift | 18 +- .../MediaPlayer/Sources/MediaPlayer.swift | 12 +- .../Sources/MediaPlayerFramePreview.swift | 12 +- .../Sources/MediaPlayerScrubbingNode.swift | 34 + .../Sources/TimeBasedVideoPreload.swift | 4 +- .../UniversalSoftwareVideoSource.swift | 22 +- .../Sources/AddPaymentMethodSheetScreen.swift | 4 +- .../Sources/AvatarGalleryController.swift | 124 +- .../AvatarGalleryItemFooterContentNode.swift | 10 +- .../Sources/PeerAvatarImageGalleryItem.swift | 26 +- .../Sources/PeerInfoAvatarListNode.swift | 134 +- .../Sources/ChannelMembersController.swift | 181 +- .../Sources/DeviceContactInfoController.swift | 65 +- .../Sources/GroupStickerPackCurrentItem.swift | 4 +- .../Sources/PeerOnlineMarkerNode.swift | 2 +- .../Sources/PhotoResources.swift | 276 +- submodules/Postbox/BUILD | 1 + submodules/Postbox/Package.swift | 2 + submodules/Postbox/Sources/MediaBox.swift | 404 ++- submodules/Postbox/Sources/MediaBoxFile.swift | 18 +- .../Postbox/Sources/MediaResource.swift | 26 +- .../Postbox/Sources/MessageHistoryTable.swift | 58 + submodules/Postbox/Sources/Postbox.swift | 9 + .../Postbox/Sources/SqliteValueBox.swift | 140 +- .../Sources/StorageBox/StorageBox.swift | 962 ++++++ .../Postbox/Sources/TimeBasedCleanup.swift | 77 +- submodules/Postbox/Sources/ValueBoxKey.swift | 9 + .../Sources/PhoneDemoComponent.swift | 1 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 4 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 6 +- .../Sources/PremiumIntroScreen.swift | 29 +- .../Sources/PremiumLimitScreen.swift | 4 +- .../Sources/PremiumLimitsListScreen.swift | 27 +- .../Sources/StickersCarouselComponent.swift | 6 +- .../QrCodeUI/Sources/QrCodeScanScreen.swift | 48 +- .../Sources/ReactionContextNode.swift | 17 +- .../Sources/ReactionSelectionNode.swift | 8 +- .../Sources/SaveToCameraRoll.swift | 20 +- .../Sources/SegmentedControlNode.swift | 49 +- submodules/SettingsUI/BUILD | 2 + .../Sources/CachedFaqInstantPage.swift | 2 +- .../Sources/ChangePhoneNumberController.swift | 182 - .../DataAndStorageSettingsController.swift | 10 +- .../KeepMediaDurationPickerItem.swift | 20 +- .../MaximumCacheSizePickerItem.swift | 28 +- .../StorageUsageController.swift | 614 +++- .../StorageUsageExceptionsScreen.swift | 508 +++ .../DeleteAccountOptionsController.swift | 4 +- .../Sources/LogoutOptionsController.swift | 4 +- .../PrivacyAndSecurityController.swift | 23 +- .../PrivacyIntroController.swift | 4 +- .../PrivacyIntroControllerNode.swift | 2 +- .../SelectivePrivacySettingsController.swift | 212 +- ...ectivePrivacySettingsPeersController.swift | 207 +- .../InstalledStickerPacksController.swift | 7 - .../Sources/ThemeCarouselItem.swift | 2 +- .../Sources/ThemePickerController.swift | 2 +- .../Sources/ThemePickerGridItem.swift | 2 +- .../Themes/CustomWallpaperPicker.swift | 2 +- .../Sources/Themes/EditThemeController.swift | 2 +- .../Themes/SettingsThemeWallpaperNode.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../Themes/ThemeGridSearchContentNode.swift | 2 +- .../Sources/Themes/ThemeGridSearchItem.swift | 6 +- .../Themes/ThemePreviewControllerNode.swift | 4 +- .../Themes/ThemeSettingsController.swift | 2 +- .../Sources/Themes/WallpaperGalleryItem.swift | 14 +- .../Sources/ShareController.swift | 10 +- .../Sources/ShareLoadingContainerNode.swift | 2 +- .../SoftwareVideoLayerFrameManager.swift | 4 +- .../Sources/SolidRoundedButtonNode.swift | 35 +- .../Sources/SparseItemGrid.swift | 8 + .../Sources/StatsMessageItem.swift | 4 +- .../Sources/StickerPackEmojisItem.swift | 17 +- .../StickerPackPreviewController.swift | 14 +- .../Sources/StickerPackPreviewGridItem.swift | 12 +- .../Sources/StickerPackScreen.swift | 2 +- .../StickerPreviewControllerNode.swift | 2 +- .../Sources/StickerPreviewPeekContent.swift | 4 +- .../Sources/StickerResources.swift | 36 +- submodules/TelegramApi/Sources/Api0.swift | 8 +- submodules/TelegramApi/Sources/Api1.swift | 30 +- submodules/TelegramApi/Sources/Api10.swift | 54 +- submodules/TelegramApi/Sources/Api11.swift | 142 +- submodules/TelegramApi/Sources/Api12.swift | 72 +- submodules/TelegramApi/Sources/Api13.swift | 30 +- submodules/TelegramApi/Sources/Api14.swift | 62 +- submodules/TelegramApi/Sources/Api15.swift | 54 +- submodules/TelegramApi/Sources/Api16.swift | 40 +- submodules/TelegramApi/Sources/Api17.swift | 58 +- submodules/TelegramApi/Sources/Api18.swift | 48 +- submodules/TelegramApi/Sources/Api19.swift | 44 +- submodules/TelegramApi/Sources/Api2.swift | 126 +- submodules/TelegramApi/Sources/Api20.swift | 286 +- submodules/TelegramApi/Sources/Api21.swift | 142 +- submodules/TelegramApi/Sources/Api22.swift | 52 +- submodules/TelegramApi/Sources/Api23.swift | 50 +- submodules/TelegramApi/Sources/Api24.swift | 48 +- submodules/TelegramApi/Sources/Api25.swift | 66 +- submodules/TelegramApi/Sources/Api26.swift | 48 +- submodules/TelegramApi/Sources/Api27.swift | 44 +- submodules/TelegramApi/Sources/Api28.swift | 14 +- submodules/TelegramApi/Sources/Api29.swift | 49 +- submodules/TelegramApi/Sources/Api3.swift | 54 +- submodules/TelegramApi/Sources/Api4.swift | 62 +- submodules/TelegramApi/Sources/Api5.swift | 54 +- submodules/TelegramApi/Sources/Api6.swift | 56 +- submodules/TelegramApi/Sources/Api7.swift | 52 +- submodules/TelegramApi/Sources/Api8.swift | 58 +- submodules/TelegramApi/Sources/Api9.swift | 32 +- .../Sources/ManagedAudioSession.swift | 27 +- .../Sources/PresentationCall.swift | 11 +- .../Sources/VoiceChatController.swift | 32 +- submodules/TelegramCore/BUILD | 1 + submodules/TelegramCore/Package.swift | 2 + .../Sources/Account/Account.swift | 19 +- .../Sources/Account/AccountManager.swift | 1 + .../Sources/ApiUtils/ApiGroupOrChannel.swift | 4 +- .../Sources/ApiUtils/ChatContextResult.swift | 45 + .../ReplyMarkupMessageAttribute.swift | 3 + .../ApiUtils/StoreMessage_Telegram.swift | 45 +- .../ApiUtils/TelegramMediaAction.swift | 4 + .../Sources/ApiUtils/TelegramMediaFile.swift | 9 +- .../Sources/ApiUtils/TelegramMediaImage.swift | 6 +- .../Sources/ApiUtils/TelegramUser.swift | 9 +- .../Sources/MacOS/MacInternalUpdater.swift | 6 +- .../Network/FetchedMediaResource.swift | 83 +- .../Sources/Network/MultipartFetch.swift | 83 +- .../Sources/Network/MultipartUpload.swift | 2 +- .../PendingMessages/EnqueueMessage.swift | 4 + .../PendingMessageUploadedContent.swift | 18 +- .../StandaloneUploadedMedia.swift | 2 +- .../Settings/CacheStorageSettings.swift | 15 +- .../State/AccountStateManagementUtils.swift | 20 +- .../Sources/State/ApplyUpdateMessage.swift | 6 +- .../Sources/State/AvailableReactions.swift | 2 +- .../TelegramCore/Sources/State/Fetch.swift | 8 +- ...onizeInstalledStickerPacksOperations.swift | 2 + ...ecretChatIncomingDecryptedOperations.swift | 48 +- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/StickerManagement.swift | 127 +- .../Sources/State/UpdatesApiUtils.swift | 2 - .../SyncCore_CacheStorageSettings.swift | 124 +- .../SyncCore/SyncCore_CachedChannelData.swift | 91 +- .../SyncCore/SyncCore_CachedUserData.swift | 54 +- ...yncCore_MediaSpoilerMessageAttribute.swift | 16 + .../SyncCore/SyncCore_Namespaces.swift | 7 + ...SyncCore_ReplyMarkupMessageAttribute.swift | 1 + .../SyncCore_TelegramMediaAction.swift | 13 + .../SyncCore/SyncCore_TelegramMediaFile.swift | 17 +- .../SyncCore_TelegramMediaImage.swift | 10 +- .../TelegramEngineAccountData.swift | 12 +- .../Contacts/TelegramEngineContacts.swift | 5 + .../Messages/AttachMenuBots.swift | 75 +- .../Messages/DeleteMessages.swift | 10 +- ...OutgoingMessageWithChatContextResult.swift | 4 +- .../Messages/ScheduledMessages.swift | 2 +- .../Messages/TelegramEngineMessages.swift | 4 +- .../Peers/ChannelAdminEventLogs.swift | 3 + .../Peers/NotificationSoundList.swift | 4 +- .../Peers/PeerPhotoUpdater.swift | 160 +- .../Peers/TelegramEnginePeers.swift | 32 + .../Peers/UpdateCachedPeerData.swift | 13 +- .../Resources/CollectCacheUsageStats.swift | 746 +++- .../Resources/TelegramEngineResources.swift | 53 + .../TelegramEngine/Stickers/StickerPack.swift | 8 +- .../Stickers/StickerSetInstallation.swift | 7 +- .../Utils/AutomaticCacheEviction.swift | 202 ++ .../ChatControllerBackgroundNode.swift | 4 +- .../Sources/DefaultDayPresentationTheme.swift | 3 +- .../Resources/PresentationResourceKey.swift | 1 + .../PresentationResourcesItemList.swift | 6 + .../Sources/ServiceMessageStrings.swift | 8 + submodules/TelegramUI/BUILD | 5 + .../ChatControllerInteraction/BUILD | 33 + .../Sources/ChatControllerInteraction.swift | 281 +- .../ChatEntityKeyboardInputNode/BUILD | 45 + .../Sources/ChatEntityKeyboardInputNode.swift | 921 ++--- .../Sources/GifPaneSearchContentNode.swift | 112 +- .../Sources/PaneSearchBarNode.swift | 1 + .../Sources/PaneSearchContainerNode.swift | 29 +- .../StickerPaneSearchContentNode.swift | 18 +- .../TelegramUI/Components/ChatInputNode/BUILD | 21 + .../ChatInputNode/Sources/ChatInputNode.swift | 34 + .../ChatTitleView/Sources/ChatTitleView.swift | 2 +- .../Sources/EmojiStatusComponent.swift | 8 +- .../EmojiStatusSelectionComponent.swift | 36 +- .../Sources/EmojiSuggestionsComponent.swift | 6 +- .../Sources/EmojiTextAttachmentView.swift | 137 +- .../Components/EntityKeyboard/BUILD | 1 + .../Sources/EmojiPagerContentComponent.swift | 760 ++++- .../Sources/EntityKeyboard.swift | 424 ++- .../EntityKeyboardTopPanelComponent.swift | 11 +- .../Sources/GifPagerContentComponent.swift | 10 +- .../Sources/ForumCreateTopicScreen.swift | 11 +- .../Sources/LottieAnimationCache.swift | 23 +- .../Components/MultiplexedVideoNode/BUILD | 26 + .../Sources/MultiplexedVideoNode.swift | 67 +- .../Sources/SoftwareVideoThumbnailLayer.swift | 14 +- .../Components/StorageUsageScreen/BUILD | 45 + .../Sources/PieChartComponent.swift | 582 ++++ .../Sources/StorageCategoriesComponent.swift | 265 ++ .../StorageCategoryItemCompoment.swift | 407 +++ .../StorageFileListPanelComponent.swift | 999 ++++++ .../Sources/StorageKeepSizeComponent.swift | 208 ++ .../StoragePeerListPanelComponent.swift | 604 ++++ .../StoragePeerTypeItemComponent.swift | 276 ++ .../StorageUsagePanelContainerComponent.swift | 760 +++++ .../Sources/StorageUsageScreen.swift | 2636 +++++++++++++++ ...geUsageScreenSelectionPanelComponent.swift | 157 + .../Sources/TextNodeWithEntities.swift | 6 +- .../Sources/VideoAnimationCache.swift | 2 +- .../Contents.json | 15 + .../EntityInputMasksIcon.imageset/mask.pdf | Bin 0 -> 5801 bytes .../Media Editor/Add.imageset/Contents.json | 21 + .../Media Editor/Add.imageset/add.png | Bin 0 -> 245 bytes .../Media Editor/Contents.json | 9 + .../Media Editor/Done.imageset/Contents.json | 12 + .../Done.imageset/ic_editor_check (2).pdf | Bin 0 -> 4104 bytes .../Eyedropper.imageset/Contents.json | 21 + .../Eyedropper.imageset/eyedropper.png | Bin 0 -> 581 bytes .../Media Editor/Fill.imageset/Contents.json | 21 + .../Media Editor/Fill.imageset/shapeFill.png | Bin 0 -> 504 bytes .../Media Editor/Flip.imageset/Contents.json | 21 + .../Media Editor/Flip.imageset/shapeFlip.png | Bin 0 -> 714 bytes .../FontArrow.imageset/Contents.json | 12 + .../FontArrow.imageset/expand.pdf | Bin 0 -> 2058 bytes .../Grayscale.imageset/Contents.json | 21 + .../Grayscale.imageset/grayscale.png | Bin 0 -> 787 bytes .../Media Editor/Redo.imageset/Contents.json | 21 + .../Media Editor/Redo.imageset/undo.png | Bin 0 -> 2175 bytes .../RoundSpectrum.imageset/Contents.json | 21 + .../RoundSpectrum.imageset/spectrum.png | Bin 0 -> 8976 bytes .../ShapeArrow.imageset/Contents.json | 21 + .../ShapeArrow.imageset/shapeArrow.png | Bin 0 -> 293 bytes .../ShapeBubble.imageset/Contents.json | 21 + .../ShapeBubble.imageset/shapeBubble.png | Bin 0 -> 475 bytes .../ShapeEllipse.imageset/Contents.json | 21 + .../ShapeEllipse.imageset/shapeEllipse.png | Bin 0 -> 556 bytes .../ShapeRectangle.imageset/Contents.json | 21 + .../shapeRectangle.png | Bin 0 -> 300 bytes .../ShapeStar.imageset/Contents.json | 21 + .../ShapeStar.imageset/shapeStar.png | Bin 0 -> 626 bytes .../Spectrum.imageset/Contents.json | 21 + .../Spectrum.imageset/spectrum.png | Bin 0 -> 336 bytes .../Stroke.imageset/Contents.json | 21 + .../Stroke.imageset/shapeStroke.png | Bin 0 -> 643 bytes .../TextDefault.imageset/Contents.json | 21 + .../TextDefault.imageset/default.png | Bin 0 -> 463 bytes .../TextFilled.imageset/Contents.json | 21 + .../TextFilled.imageset/filled.png | Bin 0 -> 859 bytes .../TextSemi.imageset/Contents.json | 21 + .../Media Editor/TextSemi.imageset/semi.png | Bin 0 -> 908 bytes .../TextStroke.imageset/Contents.json | 21 + .../TextStroke.imageset/stroke.png | Bin 0 -> 755 bytes .../ToolArrow.imageset/Contents.json | 21 + .../ToolArrow.imageset/arrow base.png | Bin 0 -> 5028 bytes .../ToolArrowShadow.imageset/Arrow.png | Bin 0 -> 26873 bytes .../ToolArrowShadow.imageset/Contents.json | 21 + .../ToolArrowTip.imageset/Contents.json | 21 + .../ToolArrowTip.imageset/arrow tip.png | Bin 0 -> 2156 bytes .../ToolBlur.imageset/Contents.json | 21 + .../ToolBlur.imageset/blur base.png | Bin 0 -> 8609 bytes .../ToolBlurShadow.imageset/Blur.png | Bin 0 -> 24274 bytes .../ToolBlurShadow.imageset/Contents.json | 21 + .../ToolBlurTip.imageset/Contents.json | 21 + .../ToolBlurTip.imageset/blur tip.png | Bin 0 -> 8265 bytes .../ToolEraser.imageset/Contents.json | 21 + .../ToolEraser.imageset/eraser.png | Bin 0 -> 10466 bytes .../ToolEraserShadow.imageset/Contents.json | 21 + .../ToolEraserShadow.imageset/Eraser.png | Bin 0 -> 25395 bytes .../ToolMarker.imageset/Contents.json | 21 + .../ToolMarker.imageset/brush.png | Bin 0 -> 8433 bytes .../ToolMarkerShadow.imageset/Brush.png | Bin 0 -> 24358 bytes .../ToolMarkerShadow.imageset/Contents.json | 21 + .../ToolMarkerTip.imageset/Contents.json | 21 + .../ToolMarkerTip.imageset/brush.png | Bin 0 -> 387 bytes .../ToolNeon.imageset/Contents.json | 21 + .../Media Editor/ToolNeon.imageset/neon.png | Bin 0 -> 8428 bytes .../ToolNeonShadow.imageset/Contents.json | 21 + .../ToolNeonShadow.imageset/Neon.png | Bin 0 -> 24298 bytes .../ToolNeonTip.imageset/Contents.json | 21 + .../ToolNeonTip.imageset/neon.png | Bin 0 -> 5395 bytes .../ToolPen.imageset/Contents.json | 21 + .../Media Editor/ToolPen.imageset/pen.png | Bin 0 -> 9188 bytes .../ToolPenShadow.imageset/Contents.json | 21 + .../ToolPenShadow.imageset/Pen.png | Bin 0 -> 23540 bytes .../ToolPenTip.imageset/Contents.json | 21 + .../Media Editor/ToolPenTip.imageset/pen.png | Bin 0 -> 422 bytes .../Media Editor/Undo.imageset/Contents.json | 21 + .../Media Editor/Undo.imageset/undo.png | Bin 0 -> 409 bytes .../ZoomOut.imageset/Contents.json | 21 + .../Media Editor/ZoomOut.imageset/zoomOut.png | Bin 0 -> 418 bytes .../AlertArrow.imageset/Contents.json | 12 + .../AlertArrow.imageset/arrow (2).pdf | Bin 0 -> 2592 bytes .../SuggestAvatar.imageset/Contents.json | 12 + .../suggestphoto_30.pdf | Bin 0 -> 11665 bytes .../Settings/SetAvatar.imageset/Contents.json | 2 +- .../Settings/SetAvatar.imageset/Icon-23.pdf | Bin 3394 -> 0 bytes .../SetAvatar.imageset/addphoto_30.pdf | Bin 0 -> 5885 bytes .../Resources/Animations/anim_spoiler.json | 1 + .../Animations/media_backToCancel.json | 1 + .../TelegramUI/Sources/AppDelegate.swift | 108 + .../Sources/ChatAvatarNavigationNode.swift | 26 +- .../TelegramUI/Sources/ChatBotInfoItem.swift | 6 +- .../Sources/ChatButtonKeyboardInputNode.swift | 2 + .../ChatContextResultPeekContentNode.swift | 9 +- .../TelegramUI/Sources/ChatController.swift | 255 +- .../Sources/ChatControllerNode.swift | 30 +- .../Sources/ChatHistoryListNode.swift | 1 + .../ChatHistorySearchContainerNode.swift | 1 + .../Sources/ChatInputContextPanelNode.swift | 1 + .../TelegramUI/Sources/ChatInputNode.swift | 34 - .../ChatInterfaceInputContextPanels.swift | 1 + .../Sources/ChatInterfaceInputNodes.swift | 3 + .../ChatInterfaceStateAccessoryPanels.swift | 1 + .../ChatInterfaceStateContextMenus.swift | 13 +- .../ChatInterfaceStateContextQueries.swift | 4 +- .../ChatInterfaceTitlePanelNodes.swift | 1 + .../Sources/ChatMediaInputGifPane.swift | 14 +- .../Sources/ChatMediaInputGridEntries.swift | 2 + .../ChatMediaInputMetaSectionItemNode.swift | 3 +- .../Sources/ChatMediaInputNode.swift | 47 +- .../Sources/ChatMediaInputPane.swift | 1 + .../Sources/ChatMediaInputPanelEntries.swift | 1 + .../ChatMediaInputPeerSpecificItem.swift | 1 + .../ChatMediaInputRecentGifsItem.swift | 1 + .../Sources/ChatMediaInputSettingsItem.swift | 1 + .../ChatMediaInputStickerGridItem.swift | 12 +- .../ChatMediaInputStickerPackItem.swift | 5 +- .../Sources/ChatMediaInputStickerPane.swift | 2 + .../Sources/ChatMediaInputTrendingItem.swift | 1 + .../Sources/ChatMessageActionItemNode.swift | 6 +- .../ChatMessageAnimatedStickerItemNode.swift | 22 +- .../ChatMessageAttachedContentNode.swift | 3 +- .../ChatMessageBubbleContentNode.swift | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 16 +- .../Sources/ChatMessageDateHeader.swift | 3 +- .../Sources/ChatMessageGiftItemNode.swift | 1 + .../ChatMessageInstantVideoItemNode.swift | 1 + .../ChatMessageInteractiveFileNode.swift | 3 +- ...atMessageInteractiveInstantVideoNode.swift | 6 +- .../ChatMessageInteractiveMediaNode.swift | 193 +- .../TelegramUI/Sources/ChatMessageItem.swift | 1 + .../Sources/ChatMessageItemView.swift | 1 + .../ChatMessageMediaBubbleContentNode.swift | 1 + .../Sources/ChatMessageNotificationItem.swift | 6 +- ...ageProfilePhotoSuggestionContentNode.swift | 319 ++ ...hatMessageReactionsFooterContentNode.swift | 1 + .../Sources/ChatMessageReplyInfoNode.swift | 8 +- .../Sources/ChatMessageStickerItemNode.swift | 5 +- .../Sources/ChatMessageSwipeToReplyNode.swift | 1 + .../ChatMessageTextBubbleContentNode.swift | 2 +- .../Sources/ChatMessageThreadInfoNode.swift | 1 + .../Sources/ChatMessageTransitionNode.swift | 4 +- .../ChatPinnedMessageTitlePanelNode.swift | 12 +- .../TelegramUI/Sources/ChatQrCodeScreen.swift | 14 +- .../ChatRecentActionsControllerNode.swift | 4 +- .../Sources/ChatRecentActionsEmptyNode.swift | 38 + .../ChatRecentActionsHistoryTransition.swift | 22 + .../ChatRecordingPreviewInputPanelNode.swift | 4 +- .../Sources/ChatReplyCountItem.swift | 1 + .../ChatTextInputActionButtonsNode.swift | 1 + .../Sources/ChatTextInputPanelNode.swift | 70 +- .../TelegramUI/Sources/ChatThemeScreen.swift | 4 +- .../TelegramUI/Sources/ChatUnreadItem.swift | 1 + .../CommandChatInputContextPanelNode.swift | 1 + ...CommandMenuChatInputContextPanelNode.swift | 1 + .../Sources/CreateChannelController.swift | 12 +- .../Sources/CreateGroupController.swift | 12 +- ...textResultsChatInputContextPanelNode.swift | 1 + .../Sources/DrawingStickersScreen.swift | 1415 -------- .../Sources/EditAccessoryPanelNode.swift | 8 +- .../EmojisChatInputContextPanelNode.swift | 3 +- .../Sources/EmojisChatInputPanelItem.swift | 1 + .../TelegramUI/Sources/GridMessageItem.swift | 5 +- .../HashtagChatInputContextPanelNode.swift | 1 + ...textResultsChatInputContextPanelNode.swift | 1 + ...ListContextResultsChatInputPanelItem.swift | 11 +- .../Sources/HorizontalStickerGridItem.swift | 10 +- ...rizontalStickersChatContextPanelNode.swift | 1 + .../Sources/InChatPrefetchManager.swift | 2 +- .../Sources/InlineReactionSearchPanel.swift | 1 + .../Sources/LargeEmojiActionSheetItem.swift | 4 +- .../TelegramUI/Sources/LegacyCamera.swift | 9 +- .../LegacyInstantVideoController.swift | 2 +- .../Sources/ManagedDiceAnimationNode.swift | 2 +- .../MentionChatInputContextPanelNode.swift | 1 + .../Sources/MultiScaleTextNode.swift | 15 +- .../Sources/NotificationContentContext.swift | 14 +- .../TelegramUI/Sources/OpenChatMessage.swift | 4 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 16 +- .../OverlayAudioPlayerControllerNode.swift | 1 + .../Sources/OverlayPlayerControlsNode.swift | 21 +- .../ListItems/PeerInfoScreenActionItem.swift | 29 +- .../PeerInfoGroupsInCommonPaneNode.swift | 1 + .../PeerInfo/Panes/PeerInfoListPaneNode.swift | 1 + .../Panes/PeerInfoVisualMediaPaneNode.swift | 74 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 147 +- .../Sources/PeerInfo/PeerInfoMembers.swift | 18 +- .../PeerInfo/PeerInfoPaneContainerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 599 +++- .../PhotoUpdateConfirmationController.swift | 244 ++ .../Sources/PeerInfoGifPaneNode.swift | 10 +- .../Sources/PeerSelectionController.swift | 8 +- .../TelegramUI/Sources/PrefetchManager.swift | 4 +- .../PreparedChatHistoryViewTransition.swift | 1 + .../Sources/ReplyAccessoryPanelNode.swift | 8 +- .../Sources/ShareExtensionContext.swift | 6 +- .../Sources/SharedAccountContext.swift | 8 +- .../Sources/SharedMediaPlayer.swift | 6 +- .../StickerPaneTrendingListGridItem.swift | 5 +- .../StickersChatInputContextPanelItem.swift | 4 +- .../StickersChatInputContextPanelNode.swift | 1 + .../Sources/TelegramRootController.swift | 17 +- .../TelegramUI/Sources/TextLinkHandling.swift | 2 +- .../Sources/ThemeUpdateManager.swift | 2 +- .../TransformOutgoingMessageMedia.swift | 10 +- ...textResultsChatInputContextPanelNode.swift | 1 + ...ListContextResultsChatInputPanelItem.swift | 6 +- .../Sources/ExperimentalUISettings.swift | 10 +- .../Sources/PostboxKeys.swift | 2 + .../Sources/NativeVideoContent.swift | 18 +- .../Sources/PlatformVideoContent.swift | 12 +- .../Sources/SystemVideoContent.swift | 10 +- .../Sources/WebEmbedVideoContent.swift | 10 +- .../Sources/GroupCallContext.swift | 124 +- .../Sources/OngoingCallContext.swift | 35 + .../ChannelMemberCategoryListContext.swift | 2 +- ...annelMemberCategoriesContextsManager.swift | 12 + .../Sources/ChatTextInputAttributes.swift | 35 +- .../OngoingCallThreadLocalContext.h | 1 + .../Sources/OngoingCallThreadLocalContext.mm | 36 +- submodules/TgVoipWebrtc/tgcalls | 2 +- .../Source/UIKitRuntimeUtils/UIKitUtils.m | 1 + .../UIViewController+Navigation.m | 6 +- .../Sources/UndoOverlayController.swift | 4 +- .../Sources/UndoOverlayControllerNode.swift | 19 +- submodules/Utils/DarwinDirStat/BUILD | 21 + submodules/Utils/DarwinDirStat/Package.swift | 32 + .../DarwinDirStat/DarwinDirStat.h | 14 + .../DarwinDirStat/Sources/DarwinDirStat.m | 2 + .../Sources/WallpaperBackgroundNode.swift | 6 +- .../Sources/WallpaperResources.swift | 18 +- .../Sources/WatchRequestHandlers.swift | 10 +- .../Sources/LegacyWebSearchEditor.swift | 3 +- .../Sources/LegacyWebSearchGallery.swift | 32 +- .../Sources/WebSearchController.swift | 53 +- .../Sources/WebSearchControllerNode.swift | 3 +- .../Sources/WebSearchGalleryController.swift | 6 +- .../WebSearchUI/Sources/WebSearchItem.swift | 6 +- submodules/WebUI/BUILD | 2 + .../Sources/WebAppAlertContentNode.swift | 82 +- .../WebUI/Sources/WebAppController.swift | 81 +- third-party/webrtc/webrtc | 2 +- versions.json | 2 +- 738 files changed, 41292 insertions(+), 17723 deletions(-) delete mode 100644 Nicegram/NGLotteryUI/Sources/Presentation/Banners/BannerHelpers.swift create mode 100644 submodules/ChatPresentationInterfaceState/Sources/ChatMediaInputNodeInteraction.swift create mode 100644 submodules/DrawingUI/BUILD create mode 100644 submodules/DrawingUI/MetalResources/Drawing.metal create mode 100644 submodules/DrawingUI/Resources/marker.png create mode 100644 submodules/DrawingUI/Resources/shape_arrow.json create mode 100644 submodules/DrawingUI/Resources/shape_circle.json create mode 100644 submodules/DrawingUI/Resources/shape_rectangle.json create mode 100644 submodules/DrawingUI/Resources/shape_star.json create mode 100644 submodules/DrawingUI/Sources/ColorPickerScreen.swift create mode 100644 submodules/DrawingUI/Sources/ConcaveHull.swift create mode 100644 submodules/DrawingUI/Sources/DrawingBubbleEntity.swift create mode 100644 submodules/DrawingUI/Sources/DrawingEntitiesView.swift create mode 100644 submodules/DrawingUI/Sources/DrawingGesture.swift create mode 100644 submodules/DrawingUI/Sources/DrawingMetalView.swift create mode 100644 submodules/DrawingUI/Sources/DrawingNeonTool.swift create mode 100644 submodules/DrawingUI/Sources/DrawingPenTool.swift create mode 100644 submodules/DrawingUI/Sources/DrawingScreen.swift create mode 100644 submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift create mode 100644 submodules/DrawingUI/Sources/DrawingStickerEntity.swift create mode 100644 submodules/DrawingUI/Sources/DrawingTextEntity.swift create mode 100644 submodules/DrawingUI/Sources/DrawingTools.swift create mode 100644 submodules/DrawingUI/Sources/DrawingUtils.swift create mode 100644 submodules/DrawingUI/Sources/DrawingVectorEntity.swift create mode 100644 submodules/DrawingUI/Sources/DrawingView.swift create mode 100644 submodules/DrawingUI/Sources/EyedropperView.swift create mode 100644 submodules/DrawingUI/Sources/ModeAndSizeComponent.swift create mode 100644 submodules/DrawingUI/Sources/StickerPickerScreen.swift create mode 100644 submodules/DrawingUI/Sources/TextSettingsComponent.swift create mode 100644 submodules/DrawingUI/Sources/ToolsComponent.swift create mode 100644 submodules/DrawingUI/Sources/Unistroke.swift create mode 100644 submodules/FeaturedStickersScreen/BUILD create mode 100644 submodules/FeaturedStickersScreen/Sources/ChatMediaInputPane.swift rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/ChatMediaInputTrendingPane.swift (88%) rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/FeaturedStickersScreen.swift (98%) rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/MediaInputPaneTrendingItem.swift (91%) rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/PaneSearchBarPlaceholderItem.swift (79%) rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/StickerPaneSearchGlobaltem.swift (90%) rename submodules/{TelegramUI => FeaturedStickersScreen}/Sources/StickerPaneSearchStickerItem.swift (81%) delete mode 100644 submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintUndoManager.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintArrowBrush.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintArrowBrush.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBrush.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBrush.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBrushPreview.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBrushPreview.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBuffers.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintBuffers.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintCanvas.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintCanvas.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintFaceDebugView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintFaceDebugView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintInput.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintInput.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintNeonBrush.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintNeonBrush.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintPath.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintPath.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintRadialBrush.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintRadialBrush.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintRender.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintRender.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintShaderSet.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintShaderSet.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintSlice.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintSlice.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintState.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintState.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintSwatch.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintSwatch.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintTexture.h delete mode 100644 submodules/LegacyComponents/Sources/TGPaintTexture.m delete mode 100644 submodules/LegacyComponents/Sources/TGPaintUndoManager.m delete mode 100644 submodules/LegacyComponents/Sources/TGPainting.h delete mode 100644 submodules/LegacyComponents/Sources/TGPainting.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.m rename submodules/LegacyComponents/Sources/{TGPhotoPaintController.h => TGPhotoDrawingController.h} (74%) create mode 100644 submodules/LegacyComponents/Sources/TGPhotoDrawingController.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintController.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintEntity.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintEntityView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintFont.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintFont.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintStickerEntity.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoTextEntityView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.h delete mode 100644 submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.m delete mode 100644 submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift create mode 100644 submodules/Postbox/Sources/StorageBox/StorageBox.swift create mode 100644 submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaSpoilerMessageAttribute.swift create mode 100644 submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift create mode 100644 submodules/TelegramUI/Components/ChatControllerInteraction/BUILD rename submodules/TelegramUI/{ => Components/ChatControllerInteraction}/Sources/ChatControllerInteraction.swift (59%) create mode 100644 submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD rename submodules/TelegramUI/{ => Components/ChatEntityKeyboardInputNode}/Sources/ChatEntityKeyboardInputNode.swift (74%) rename submodules/TelegramUI/{ => Components/ChatEntityKeyboardInputNode}/Sources/GifPaneSearchContentNode.swift (65%) rename submodules/TelegramUI/{ => Components/ChatEntityKeyboardInputNode}/Sources/PaneSearchBarNode.swift (99%) rename submodules/TelegramUI/{ => Components/ChatEntityKeyboardInputNode}/Sources/PaneSearchContainerNode.swift (83%) rename submodules/TelegramUI/{ => Components/ChatEntityKeyboardInputNode}/Sources/StickerPaneSearchContentNode.swift (97%) create mode 100644 submodules/TelegramUI/Components/ChatInputNode/BUILD create mode 100644 submodules/TelegramUI/Components/ChatInputNode/Sources/ChatInputNode.swift create mode 100644 submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD rename submodules/TelegramUI/{ => Components/MultiplexedVideoNode}/Sources/MultiplexedVideoNode.swift (93%) rename submodules/TelegramUI/{ => Components/MultiplexedVideoNode}/Sources/SoftwareVideoThumbnailLayer.swift (87%) create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/BUILD create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/mask.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Add.imageset/add.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Eyedropper.imageset/eyedropper.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/expand.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/grayscale.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/spectrum.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Spectrum.imageset/spectrum.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Stroke.imageset/shapeStroke.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextDefault.imageset/default.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextSemi.imageset/semi.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/TextStroke.imageset/stroke.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/arrow base.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/arrow tip.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/blur base.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/blur tip.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarker.imageset/brush.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerTip.imageset/brush.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPen.imageset/pen.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenTip.imageset/pen.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/arrow (2).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/suggestphoto_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Settings/SetAvatar.imageset/Icon-23.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/SetAvatar.imageset/addphoto_30.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/anim_spoiler.json create mode 100644 submodules/TelegramUI/Resources/Animations/media_backToCancel.json delete mode 100644 submodules/TelegramUI/Sources/ChatInputNode.swift create mode 100644 submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift delete mode 100644 submodules/TelegramUI/Sources/DrawingStickersScreen.swift create mode 100644 submodules/TelegramUI/Sources/PeerInfo/PhotoUpdateConfirmationController.swift create mode 100644 submodules/Utils/DarwinDirStat/BUILD create mode 100644 submodules/Utils/DarwinDirStat/Package.swift create mode 100644 submodules/Utils/DarwinDirStat/PublicHeaders/DarwinDirStat/DarwinDirStat.h create mode 100644 submodules/Utils/DarwinDirStat/Sources/DarwinDirStat.m diff --git a/Nicegram/NGAppCache/Sources/AppCache.swift b/Nicegram/NGAppCache/Sources/AppCache.swift index 88eb945c42f..8c172fce42b 100644 --- a/Nicegram/NGAppCache/Sources/AppCache.swift +++ b/Nicegram/NGAppCache/Sources/AppCache.swift @@ -29,6 +29,9 @@ public final class AppCache { @UserDefaultsBacked(key: "wasLotteryShown", storage: .standard, defaultValue: false) public static var wasLotteryShown: Bool + + @UserDefaultsBacked(key: "lastSeenBlockedChatId", storage: .standard, defaultValue: nil) + public static var lastSeenBlockedChatId: Int64? public static var wasLauchedBefore: Bool { get { diff --git a/Nicegram/NGLotteryUI/Sources/Presentation/Banners/BannerHelpers.swift b/Nicegram/NGLotteryUI/Sources/Presentation/Banners/BannerHelpers.swift deleted file mode 100644 index 84ff9e311c0..00000000000 --- a/Nicegram/NGLotteryUI/Sources/Presentation/Banners/BannerHelpers.swift +++ /dev/null @@ -1,21 +0,0 @@ -import NGCore -import NGToast - -public func showLotteryBannerAsToast(jackpot: Money, onTap: @escaping () -> Void) { - let banner = LotteryBannerView() - banner.display(jackpot: jackpot) - - let toast = NGToast(topInsetFromSafeArea: 65) - toast.duration = nil - toast.setContentView(banner) - - banner.onTap = { [weak toast] in - toast?.hide() - onTap() - } - banner.onClose = { [weak toast] in - toast?.hide() - } - - toast.show() -} diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index 2de80052d61..cf27c1d14c7 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -580,10 +580,9 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.secretMenu("Secret Menu")) } - if !hideUnblock, - let url = URL(string: "https://my.nicegram.app") { + if !hideUnblock { entries.append(.unblockHeader(l("NicegramSettings.Unblock.Header", locale).uppercased())) - entries.append(.unblock(l("NicegramSettings.Unblock.Button", locale), url)) + entries.append(.unblock(l("NicegramSettings.Unblock.Button", locale), nicegramUnblockUrl)) } entries.append(.TabsHeader(l("NicegramSettings.Tabs", diff --git a/Nicegram/NGUtils/Sources/MediaResources.swift b/Nicegram/NGUtils/Sources/MediaResources.swift index dc15aec544d..d3234c93efc 100644 --- a/Nicegram/NGUtils/Sources/MediaResources.swift +++ b/Nicegram/NGUtils/Sources/MediaResources.swift @@ -4,7 +4,11 @@ import SwiftSignalKit import TelegramCore import Foundation -public func fetchResource(mediaResourceReference: MediaResourceReference, context: AccountContext) -> Signal { +public func fetchResource( + mediaResourceReference: MediaResourceReference, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, + context: AccountContext) -> Signal { let resource = mediaResourceReference.resource let mediaBox = context.account.postbox.mediaBox @@ -29,7 +33,7 @@ public func fetchResource(mediaResourceReference: MediaResourceReference, contex }, completed: { subscriber.putCompletion() }) - let fetchedDataDisposable = fetchedMediaResource(mediaBox: mediaBox, reference: mediaResourceReference).start() + let fetchedDataDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaResourceReference).start() return ActionDisposable { resourceDataDisposable.dispose() fetchedDataDisposable.dispose() @@ -52,5 +56,9 @@ public func fetchAvatarImage(peer: Peer, context: AccountContext) -> Signal Int64 { diff --git a/Random.txt b/Random.txt index e1f36b13403..c6648d1db0d 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -c190c625502c4d5a9b1ac0d016da64ce +44eed22b384449bcec8962f2fddbfebd diff --git a/Telegram/BUILD b/Telegram/BUILD index 5e37305c689..e0835a4f6b4 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -366,6 +366,7 @@ swift_library( "//submodules/PasswordSetupUI:PasswordSetupUIResources", "//submodules/PasswordSetupUI:PasswordSetupUIAssets", "//submodules/PremiumUI:PremiumUIResources", + "//submodules/DrawingUI:DrawingUIResources", "//submodules/TelegramUI:TelegramUIResources", "//submodules/TelegramUI:TelegramUIAssets", ":GeneratedPresentationStrings/Resources/PresentationStrings.data", @@ -441,9 +442,9 @@ official_apple_pay_merchants = [ "merchant.org.telegram.Best2pay.test", "merchant.psbank.test.telegramios", "merchant.psbank.prod.telegramios", - #"merchant.org.telegram.billinenet.test", - #"merchant.org.telegram.billinenet.prod", - #"merchant.org.telegram.portmone.test", + "merchant.org.telegram.billinenet.test", + "merchant.org.telegram.billinenet.prod", + "merchant.org.telegram.portmone.test", ] official_bundle_ids = [ @@ -1862,6 +1863,7 @@ plist_fragment( BGTaskSchedulerPermittedIdentifiers {telegram_bundle_id}.refresh + {telegram_bundle_id}.cleanup CFBundleAllowMixedLocalizations @@ -1962,6 +1964,7 @@ plist_fragment( location remote-notification voip + processing UIDeviceFamily diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 7693e0865bc..53cbbe9470c 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1145,14 +1145,20 @@ private final class NotificationServiceHandler { var fetchMediaSignal: Signal = .single(nil) if let mediaAttachment = mediaAttachment { + var contentType: MediaResourceUserContentType = .other var fetchResource: TelegramMultipartFetchableResource? if let image = mediaAttachment as? TelegramMediaImage, let representation = largestImageRepresentation(image.representations), let resource = representation.resource as? TelegramMultipartFetchableResource { fetchResource = resource + contentType = .image } else if let file = mediaAttachment as? TelegramMediaFile { if file.isSticker { fetchResource = file.resource as? TelegramMultipartFetchableResource + contentType = .other } else if file.isVideo { fetchResource = file.previewRepresentations.first?.resource as? TelegramMultipartFetchableResource + contentType = .video + } else { + contentType = .file } } @@ -1172,6 +1178,13 @@ private final class NotificationServiceHandler { parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), + location: messageId.flatMap { messageId in + return MediaResourceStorageLocation( + peerId: peerId, + messageId: messageId + ) + }, + contentType: contentType, isRandomAccessAllowed: true ), encryptionKey: nil, @@ -1223,6 +1236,8 @@ private final class NotificationServiceHandler { parameters: MediaResourceFetchParameters( tag: nil, info: resourceFetchInfo(resource: resource), + location: nil, + contentType: .other, isRandomAccessAllowed: true ), encryptionKey: nil, diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 61bd8149909..84e8c0fd23c 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -253,6 +253,9 @@ "PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice"; "PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF"; +"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@ suggested you new profile photo"; + + "PUSH_REMINDER_TITLE" = "🗓 Reminder"; "PUSH_SENDER_YOU" = "📅 You"; @@ -7299,6 +7302,7 @@ Sorry for the inconvenience."; "Contacts.Sort.ByLastSeen" = "by Last Seen"; "ClearCache.Progress" = "Clearing the Cache • %d%"; +"ClearCache.NoProgress" = "Clearing the Cache"; "ClearCache.KeepOpenedDescription" = "Please keep this window open until the clearing is completed."; "Share.ShareAsLink" = "Share as Link"; @@ -8161,6 +8165,7 @@ Sorry for the inconvenience."; "EmojiSearch.SearchReactionsEmptyResult" = "No emoji found"; "EmojiSearch.SearchStatusesPlaceholder" = "Search Statuses"; "EmojiSearch.SearchStatusesEmptyResult" = "No emoji found"; +"EmojiSearch.SearchEmojiEmptyResult" = "No emoji found"; "ChatList.StartAction" = "Start"; "ChatList.CloseAction" = "Close"; @@ -8443,3 +8448,156 @@ Sorry for the inconvenience."; "MessageTimer.LargeShortMonths_any" = "%@m"; "MessageTimer.LargeShortYears_1" = "%@y"; "MessageTimer.LargeShortYears_any" = "%@y"; + +"Channel.AdminLog.AntiSpamEnabled" = "%1$@ enabled anti-spam"; +"Channel.AdminLog.AntiSpamDisabled" = "%1$@ disabled anti-spam"; + +"UserInfo.SuggestPhoto" = "Suggest Photo for %@"; +"UserInfo.SetCustomPhoto" = "Set Photo for %@"; +"UserInfo.ChangeCustomPhoto" = "Change Photo for %@"; +"UserInfo.ResetCustomPhoto" = "Reset to Original Photo"; +"UserInfo.ResetCustomVideo" = "Reset to Original Video"; +"UserInfo.RemoveCustomPhoto" = "Remove Photo"; +"UserInfo.RemoveCustomVideo" = "Remove Video"; +"UserInfo.CustomPhotoInfo" = "You can replace %@’s photo with another photo that only you will see."; + +"UserInfo.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?"; +"UserInfo.SetCustomPhotoTitle" = "Do you want to set a custom profile picture for %@?"; + +"UserInfo.SuggestPhoto.AlertPhotoText" = "Do you want to suggest %@ to set this photo for his/her profile?"; +"UserInfo.SuggestPhoto.AlertVideoText" = "Do you want to suggest %@ to set this video for his/her profile?"; +"UserInfo.SuggestPhoto.AlertSuggest" = "Suggest"; + +"UserInfo.SetCustomPhoto.AlertPhotoText" = "Do you want to set this photo for %@? Only you will see this photo and it will replace any photo %@ sets for themselves."; +"UserInfo.SetCustomPhoto.AlertVideoText" = "Do you want to set this video for %@? Only you will see this video and it will replace any photo %@ sets for themselves."; +"UserInfo.SetCustomPhoto.AlertSet" = "Set"; +"UserInfo.SetCustomPhoto.SuccessPhotoText" = "You will now always see this photo for **%@** account."; +"UserInfo.SetCustomPhoto.SuccessVideoText" = "You will now always see this video for **%@** account."; + +"UserInfo.CustomPhoto" = "photo set by you"; +"UserInfo.CustomVideo" = "video set by you"; + +"UserInfo.PublicPhoto" = "public photo"; +"UserInfo.PublicVideo" = "public video"; + +"UserInfo.ResetToOriginalAlertText" = "Are you sure you want to reset to %@ original photo?"; +"UserInfo.ResetToOriginalAlertReset" = "Reset"; + +"Conversation.SuggestedPhotoTitle" = "Suggested Photo"; +"Conversation.SuggestedPhotoText" = "**%@** suggests you to use this profile photo."; +"Conversation.SuggestedPhotoTextExpanded" = "%@ suggests you to use this profile photo for your Telegram account."; +"Conversation.SuggestedPhotoTextYou" = "You suggested **%@** to use this profile photo."; +"Conversation.SuggestedPhotoView" = "View Photo"; +"Conversation.SuggestedPhotoSuccess" = "Photo updated"; +"Conversation.SuggestedPhotoSuccessText" = "You can change it in [Settings]()."; + +"Conversation.SuggestedVideoTitle" = "Suggested Video"; +"Conversation.SuggestedVideoText" = "**%@** suggests you to use this profile video."; +"Conversation.SuggestedVideoTextExpanded" = "%@ suggests you to use this profile video for your Telegram account."; +"Conversation.SuggestedVideoTextYou" = "You suggested **%@** to use this profile video."; +"Conversation.SuggestedVideoView" = "View Video"; +"Conversation.SuggestedVideoSuccess" = "Video updated"; +"Conversation.SuggestedVideoSuccessText" = "You can change it in [Settings]()."; + +"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception"; +"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions"; + +"Conversation.Messages_1" = "%@ message"; +"Conversation.Messages_any" = "%@ messages"; + +"Notification.SuggestedProfilePhoto" = "Suggested Profile Photo"; +"Notification.SuggestedProfileVideo" = "Suggested Profile Video"; + +"PhotoEditor.SetAsMyPhoto" = "Set as My Photo"; +"PhotoEditor.SetAsMyVideo" = "Set as My Video"; + +"Notification.BotWriteAllowed" = "You allowed this bot to message you when you added it in the attachment menu."; + +"Privacy.ProfilePhoto.SetPublicPhoto" = "Set Public Photo"; +"Privacy.ProfilePhoto.UpdatePublicPhoto" = "Update Public Photo"; +"Privacy.ProfilePhoto.RemovePublicPhoto" = "Remove Public Photo"; +"Privacy.ProfilePhoto.RemovePublicVideo" = "Remove Public Video"; +"Privacy.ProfilePhoto.PublicPhotoInfo" = "You can upload a public photo for those who are restricted from viewing your real profile photo."; +"Privacy.ProfilePhoto.PublicPhotoSuccess" = "This photo is now set for those who are restricted from viewing your main photo."; +"Privacy.ProfilePhoto.PublicVideoSuccess" = "This video is now set for those who are restricted from viewing your main photo."; + +"Privacy.ProfilePhoto.CustomOverrideInfo" = "You can add users or entire groups which will not see your profile photo."; +"Privacy.ProfilePhoto.CustomOverrideAddInfo" = "Add users or entire groups which will still see your profile photo."; +"Privacy.ProfilePhoto.CustomOverrideBothInfo" = "You can add users or entire groups as exceptions that will override the settings above."; + +"WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages"; + +"Common.Paste" = "Paste"; + +"PhotoEditor.SelectCoverFrameSuggestion" = "Choose a cover for this video"; + +"GroupInfo.TitleMembers_1" = "%@ Member"; +"GroupInfo.TitleMembers_any" = "%@ Members"; + +"PeerInfo.HideMembersLimitedParticipantCountText_1" = "Only groups with more than **%d member** can have their member list hidden."; +"PeerInfo.HideMembersLimitedParticipantCountText_any" = "Only groups with more than **%d members** can have their member list hidden."; +"PeerInfo.HideMembersLimitedRights" = "You don't have permission to change this setting."; + +"Privacy.Exceptions" = "EXCEPTIONS"; +"Privacy.ExceptionsCount_1" = "%@ EXCEPTION"; +"Privacy.ExceptionsCount_any" = "%@ EXCEPTIONS"; +"Privacy.Exceptions.DeleteAllExceptions" = "Delete All Exceptions"; + +"Privacy.Exceptions.DeleteAll" = "Delete All"; +"Privacy.Exceptions.DeleteAllConfirmation" = "Are you sure you want to delete all exceptions?"; + +"Attachment.EnableSpoiler" = "Hide With Spoiler"; +"Attachment.DisableSpoiler" = "Disable Spoiler"; + +"ProfilePhoto.PublicPhoto" = "public photo"; +"ProfilePhoto.PublicVideo" = "public video"; + +"Paint.Draw" = "Draw"; +"Paint.Sticker" = "Sticker"; +"Paint.Text" = "Text"; +"Paint.ZoomOut" = "Zoom Out"; + +"Paint.Rectangle" = "Rectangle"; +"Paint.Ellipse" = "Ellipse"; +"Paint.Bubble" = "Bubble"; +"Paint.Star" = "Star"; +"Paint.Arrow" = "Arrow"; + +"Paint.MoveForward" = "Move Forward"; + +"StorageManagement.Title" = "Storage Usage"; +"StorageManagement.TitleCleared" = "Storage Cleared"; + +"StorageManagement.DescriptionCleared" = "All media can be re-downloaded from the Telegram cloud if you need it again."; +"StorageManagement.DescriptionChatUsage" = "This chat uses %1$@% of your Telegram cache."; +"StorageManagement.DescriptionAppUsage" = "Telegram uses %1$@% of your free disk space."; + +"StorageManagement.ClearAll" = "Clear All Cache"; +"StorageManagement.ClearSelected" = "Clear Selected"; + +"StorageManagement.SectionPhotos" = "Photos"; +"StorageManagement.SectionVideos" = "Videos"; +"StorageManagement.SectionFiles" = "Files"; +"StorageManagement.SectionMusic" = "Music"; +"StorageManagement.SectionOther" = "Other"; +"StorageManagement.SectionStickers" = "Stickers"; +"StorageManagement.SectionAvatars" = "Avatars"; +"StorageManagement.SectionMiscellaneous" = "Miscellaneous"; + +"StorageManagement.SectionsDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; + +"StorageManagement.AutoremoveHeader" = "AUTO-REMOVE CACHED MEDIA"; +"StorageManagement.AutoremoveDescription" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space."; + +"StorageManagement.AutoremoveSpaceDescription" = "If your cache size exceeds this limit, the oldest media will be deleted."; + +"StorageManagement.TabChats" = "Chats"; +"StorageManagement.TabMedia" = "Media"; +"StorageManagement.TabFiles" = "Files"; +"StorageManagement.TabMusic" = "Music"; + +"ClearCache.Never" = "Never"; + +"GroupMembers.HideMembers" = "Hide Members"; +"GroupMembers.MembersHiddenOn" = "Switch this off to show the list of members in this group."; +"GroupMembers.MembersHiddenOff" = "Switch this on to hide the list of members in this group. Admins will remain visible."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a214279acc5..ee36edc5bc9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -706,6 +706,13 @@ public enum ChatListSearchFilter: Equatable { } } +public enum InstalledStickerPacksControllerMode { + case general + case modal + case masks + case emoji +} + public let defaultContactLabel: String = "_$!!$_" public enum CreateGroupMode { @@ -815,9 +822,11 @@ public protocol SharedAccountContext: AnyObject { func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController - + func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController + func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode) -> ViewController + func navigateToCurrentCall() var hasOngoingCall: ValuePromise { get } var immediateHasOngoingCall: Bool { get } diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 23bed51997e..2c7784e953d 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -6,8 +6,8 @@ import SwiftSignalKit import TelegramUIPreferences import RangeSet -public func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { - return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)) +public func freeMediaFileInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference) -> Signal { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) } // MARK: Nicegram downloading feature public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority, accountContext: AccountContext?) -> Signal { @@ -16,8 +16,8 @@ public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileRefe return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: RangeSet(0 ..< Int64.max), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil, accountContext: accountContext, shouldSave: false) } -public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal { - return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource)) +public func freeMediaFileResourceInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource) -> Signal { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource)) } public func cancelFreeMediaFileInteractiveFetch(account: Account, file: TelegramMediaFile) { diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 9a45af7efdb..4619663321f 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -52,8 +52,9 @@ public final class PeerSelectionControllerParams { public let multipleSelection: Bool public let forwardedMessageIds: [EngineMessage.Id] public let hasTypeHeaders: Bool + public let selectForumThreads: Bool - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) { self.context = context self.updatedPresentationData = updatedPresentationData self.filter = filter @@ -68,6 +69,7 @@ public final class PeerSelectionControllerParams { self.multipleSelection = multipleSelection self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders + self.selectForumThreads = selectForumThreads } } diff --git a/submodules/AccountUtils/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift index 34ac90309cd..d4f78b76894 100644 --- a/submodules/AccountUtils/Sources/AccountUtils.swift +++ b/submodules/AccountUtils/Sources/AccountUtils.swift @@ -4,8 +4,11 @@ import TelegramCore import TelegramUIPreferences import AccountContext -public let maximumNumberOfAccounts = 100 -public let maximumPremiumNumberOfAccounts = 100 +// MARK: Nicegram MaxAccounts +public let nicegramMaximumNumberOfAccounts = 1000 +public let maximumNumberOfAccounts = nicegramMaximumNumberOfAccounts +public let maximumPremiumNumberOfAccounts = nicegramMaximumNumberOfAccounts +// public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> { // MARK: Nicegram DB Changes diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index af8992f0c96..8d7e71b67d2 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -47,6 +47,12 @@ public final class AnimationNode : ASDisplayNode { self.colorCallbacks.append(colorCallback) view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/ } + + if let value = colors["__allcolors__"] { + for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) { + view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) + } + } } return view @@ -75,6 +81,12 @@ public final class AnimationNode : ASDisplayNode { self.colorCallbacks.append(colorCallback) view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/ } + + if let value = colors["__allcolors__"] { + for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) { + view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) + } + } } return view @@ -157,9 +169,13 @@ public final class AnimationNode : ASDisplayNode { } } - public func loop() { + public func loop(count: Int? = nil) { if let animationView = self.animationView() { - animationView.loopMode = .loop + if let count = count { + animationView.loopMode = .repeat(Float(count)) + } else { + animationView.loopMode = .loop + } animationView.play() } } diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index 1dcc729b298..91436d62566 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -437,8 +437,8 @@ public final class AppLockContextImpl: AppLockContext { public var invalidAttempts: Signal { return self.currentState.get() |> map { state in - return state.unlockAttemts.flatMap { unlockAttemts in - return AccessChallengeAttempts(count: unlockAttemts.count, bootTimestamp: unlockAttemts.timestamp.bootTimestamp, uptime: unlockAttemts.timestamp.uptime) + return state.unlockAttempts.flatMap { unlockAttempts in + return AccessChallengeAttempts(count: unlockAttempts.count, bootTimestamp: unlockAttempts.timestamp.bootTimestamp, uptime: unlockAttempts.timestamp.uptime) } } } @@ -467,7 +467,7 @@ public final class AppLockContextImpl: AppLockContext { self.updateLockState { state in var state = state - state.unlockAttemts = nil + state.unlockAttempts = nil state.isManuallyLocked = false @@ -483,16 +483,16 @@ public final class AppLockContextImpl: AppLockContext { public func failedUnlockAttempt() { self.updateLockState { state in var state = state - var unlockAttemts = state.unlockAttemts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0)) + var unlockAttempts = state.unlockAttempts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0)) - unlockAttemts.count += 1 + unlockAttempts.count += 1 var bootTimestamp: Int32 = 0 let uptime = getDeviceUptimeSeconds(&bootTimestamp) let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime) - unlockAttemts.timestamp = timestamp - state.unlockAttemts = unlockAttemts + unlockAttempts.timestamp = timestamp + state.unlockAttempts = unlockAttempts return state } } diff --git a/submodules/AppLockState/Sources/AppLockState.swift b/submodules/AppLockState/Sources/AppLockState.swift index c632725ab1d..3db956b6309 100644 --- a/submodules/AppLockState/Sources/AppLockState.swift +++ b/submodules/AppLockState/Sources/AppLockState.swift @@ -24,13 +24,13 @@ public struct UnlockAttempts: Codable, Equatable { public struct LockState: Codable, Equatable { public var isManuallyLocked: Bool public var autolockTimeout: Int32? - public var unlockAttemts: UnlockAttempts? + public var unlockAttempts: UnlockAttempts? public var applicationActivityTimestamp: MonotonicTimestamp? public init(isManuallyLocked: Bool = false, autolockTimeout: Int32? = nil, unlockAttemts: UnlockAttempts? = nil, applicationActivityTimestamp: MonotonicTimestamp? = nil) { self.isManuallyLocked = isManuallyLocked self.autolockTimeout = autolockTimeout - self.unlockAttemts = unlockAttemts + self.unlockAttempts = unlockAttemts self.applicationActivityTimestamp = applicationActivityTimestamp } } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 8b3028fd823..22e8301f4ae 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -421,7 +421,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return UIView() } - return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: 24.0, height: 24.0)) + return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: 24.0, height: 24.0)) } self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 6e1c73fc0a1..ff225291562 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -136,10 +136,16 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { if let _ = otherGestureRecognizer.view?.superview as? MKMapView { return false } + if let view = otherGestureRecognizer.view, view.description.contains("WKChildScroll") { + let velocity = panGestureRecognizer.velocity(in: nil) + if abs(velocity.x) > abs(velocity.y) * 2.0 { + return false + } + } if let _ = otherGestureRecognizer.view?.asyncdisplaykit_node as? CollectionIndexNode { return false } diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 53123bb6470..af45bf65145 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -984,7 +984,7 @@ public class AttachmentController: ViewController { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) - let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) + let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() @@ -996,7 +996,7 @@ public class AttachmentController: ViewController { disposableSet.add(accountFullSizeData.start()) } } else { - disposableSet.add(freeMediaFileInteractiveFetched(account: context.account, fileReference: .attachBot(peer: peer, media: file)).start()) + disposableSet.add(freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).start()) } } } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index ca264d22b0b..5867c574e71 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -836,7 +836,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() @@ -848,7 +848,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { self.iconDisposables[file.fileId] = accountFullSizeData.start() } } else { - self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .attachBot(peer: peer, media: file)).start() + self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).start() } } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index 46a82494b30..68e56349700 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -109,6 +109,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self?.navigationItem.rightBarButtonItem?.isEnabled = value } + self.controllerNode.present = { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + } + if let (number, email, codeType, nextType, timeout) = self.data { var appleSignInAllowed = false if case let .email(_, _, _, appleSignInAllowedValue, _) = codeType { @@ -236,7 +240,7 @@ func addTemporaryKeyboardSnapshotView(navigationController: NavigationController keyboardWindow.addSubview(snapshotView) } - Queue.mainQueue().after(0.45, { + Queue.mainQueue().after(0.5, { snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index e2e78401216..d9f82016a7b 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -14,6 +14,7 @@ import TelegramAnimatedStickerNode import SolidRoundedButtonNode import InvisibleInkDustNode import AuthorizationUtils +import TelegramStringFormatting final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextFieldDelegate { private let strings: PresentationStrings @@ -64,6 +65,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var loginWithCode: ((String) -> Void)? var signInWithApple: (() -> Void)? var openFragment: ((String) -> Void)? + var present: (ViewController, Any?) -> Void = { _, _ in } var requestNextOption: (() -> Void)? var requestAnotherOption: (() -> Void)? @@ -170,6 +172,34 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF strongSelf.textChanged(text: strongSelf.codeInputView.text) } + self.codeInputView.longPressed = { [weak self] in + guard let strongSelf = self else { + return + } + + if let code = UIPasteboard.general.string, let codeLength = strongSelf.requiredCodeLength, code.count == Int(codeLength) { + let code = normalizeArabicNumeralString(code, type: .western) + guard code.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789").inverted) == nil else { + return + } + + let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.strings.Common_Paste, accessibilityLabel: strongSelf.strings.Common_Paste), action: { [weak self] in + self?.updateCode(code) + })]) + + strongSelf.present( + controller, + ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf, strongSelf.codeInputView.frame.offsetBy(dx: 0.0, dy: -8.0), strongSelf, strongSelf.bounds) + } else { + return nil + } + }) + ) + } + } + self.nextOptionButtonNode.addTarget(self, action: #selector(self.nextOptionNodePressed), forControlEvents: .touchUpInside) self.proceedNode.pressed = { [weak self] in self?.proceedPressed() @@ -188,28 +218,34 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.view.addSubview(signInWithAppleButton) } } - + func updateCode(_ code: String) { self.codeInputView.text = code self.textChanged(text: code) + if let codeLength = self.requiredCodeLength, code.count == Int(codeLength) { + self.loginWithCode?(code) + } + } + + var requiredCodeLength: Int32? { if let codeType = self.codeType { - var codeLength: Int32? switch codeType { - case let .call(length): - codeLength = length - case let .otherSession(length): - codeLength = length - case let .missedCall(_, length): - codeLength = length - case let .sms(length): - codeLength = length - default: - break - } - if let codeLength = codeLength, code.count == Int(codeLength) { - self.loginWithCode?(code) + case let .call(length): + return length + case let .otherSession(length): + return length + case let .missedCall(_, length): + return length + case let .sms(length): + return length + case let .fragment(_, length): + return length + default: + return nil } + } else { + return nil } } diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 236b09d8fee..adddf86a6c7 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -65,11 +65,11 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?, }) var fetchedDataDisposable: Disposable? if let peerReference = peerReference { - fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start() + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start() } else if let authorOfMessage = authorOfMessage { - fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start() + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start() } else { - fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start() + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start() } return ActionDisposable { resourceDataDisposable.dispose() @@ -84,7 +84,7 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?, } } -public func peerAvatarCompleteImage(account: Account, peer: EnginePeer, size: CGSize, round: Bool = true, font: UIFont = avatarPlaceholderFont(size: 13.0), drawLetters: Bool = true, fullSize: Bool = false, blurred: Bool = false) -> Signal { +public func peerAvatarCompleteImage(account: Account, peer: EnginePeer, forceProvidedRepresentation: Bool = false, representation: TelegramMediaImageRepresentation? = nil, size: CGSize, round: Bool = true, font: UIFont = avatarPlaceholderFont(size: 13.0), drawLetters: Bool = true, fullSize: Bool = false, blurred: Bool = false) -> Signal { let iconSignal: Signal let clipStyle: AvatarNodeClipStyle @@ -97,7 +97,15 @@ public func peerAvatarCompleteImage(account: Account, peer: EnginePeer, size: CG } else { clipStyle = .none } - if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: peer.profileImageRepresentations.first, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { + + let thumbnailRepresentation: TelegramMediaImageRepresentation? + if forceProvidedRepresentation { + thumbnailRepresentation = representation + } else { + thumbnailRepresentation = peer.profileImageRepresentations.first + } + + if let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: thumbnailRepresentation, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { if fullSize, let fullSizeSignal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) { iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal)) |> mapToSignal { thumbnailImage, fullSizeImage -> Signal in diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 5e33e8e6939..3cc65db389f 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -23,14 +23,16 @@ import Markdown final class BotCheckoutControllerArguments { fileprivate let account: Account + fileprivate let source: BotPaymentInvoiceSource fileprivate let openInfo: (BotCheckoutInfoControllerFocus) -> Void fileprivate let openPaymentMethod: () -> Void fileprivate let openShippingMethod: () -> Void fileprivate let updateTip: (Int64) -> Void fileprivate let ensureTipInputVisible: () -> Void - fileprivate init(account: Account, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, updateTip: @escaping (Int64) -> Void, ensureTipInputVisible: @escaping () -> Void) { + fileprivate init(account: Account, source: BotPaymentInvoiceSource, openInfo: @escaping (BotCheckoutInfoControllerFocus) -> Void, openPaymentMethod: @escaping () -> Void, openShippingMethod: @escaping () -> Void, updateTip: @escaping (Int64) -> Void, ensureTipInputVisible: @escaping () -> Void) { self.account = account + self.source = source self.openInfo = openInfo self.openPaymentMethod = openPaymentMethod self.openShippingMethod = openShippingMethod @@ -245,7 +247,7 @@ enum BotCheckoutEntry: ItemListNodeEntry { let arguments = arguments as! BotCheckoutControllerArguments switch self { case let .header(theme, invoice, botName): - return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) + return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, source: arguments.source, botName: botName, sectionId: self.section) case let .price(_, theme, text, value, isFinal, hasSeparator, shimmeringIndex): return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: shimmeringIndex, sectionId: self.section) case let .tip(_, _, text, currency, value, numericValue, maxValue, variants): @@ -712,7 +714,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz var openShippingMethodImpl: (() -> Void)? var ensureTipInputVisibleImpl: (() -> Void)? - let arguments = BotCheckoutControllerArguments(account: context.account, openInfo: { item in + let arguments = BotCheckoutControllerArguments(account: context.account, source: source, openInfo: { item in openInfoImpl?(item) }, openPaymentMethod: { openPaymentMethodImpl?() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 48847922ce2..408d932438b 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -14,13 +14,15 @@ class BotCheckoutHeaderItem: ListViewItem, ItemListItem { let account: Account let theme: PresentationTheme let invoice: TelegramMediaInvoice + let source: BotPaymentInvoiceSource let botName: String let sectionId: ItemListSectionId - init(account: Account, theme: PresentationTheme, invoice: TelegramMediaInvoice, botName: String, sectionId: ItemListSectionId) { + init(account: Account, theme: PresentationTheme, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, botName: String, sectionId: ItemListSectionId) { self.account = account self.theme = theme self.invoice = invoice + self.source = source self.botName = botName self.sectionId = sectionId } @@ -173,7 +175,15 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { maxTextWidth = max(1.0, maxTextWidth - imageSize.width - imageTextSpacing) if imageUpdated { updatedImageSignal = chatWebFileImage(account: item.account, file: photo) - updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: .standalone(resource: photo.resource)) + + var userLocation: MediaResourceUserLocation = .other + switch item.source { + case let .message(messageId): + userLocation = .peer(messageId.peerId) + default: + break + } + updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: .standalone(resource: photo.resource)) } } diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index 137d2fac26d..319e8b4e5cf 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -12,9 +12,11 @@ import TelegramStringFormatting final class BotReceiptControllerArguments { fileprivate let account: Account + fileprivate let source: BotPaymentInvoiceSource - fileprivate init(account: Account) { + fileprivate init(account: Account, source: BotPaymentInvoiceSource) { self.account = account + self.source = source } } @@ -154,7 +156,7 @@ enum BotReceiptEntry: ItemListNodeEntry { let arguments = arguments as! BotReceiptControllerArguments switch self { case let .header(theme, invoice, botName): - return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, botName: botName, sectionId: self.section) + return BotCheckoutHeaderItem(account: arguments.account, theme: theme, invoice: invoice, source: arguments.source, botName: botName, sectionId: self.section) case let .price(_, theme, text, value, hasSeparator, isFinal): return BotCheckoutPriceItem(theme: theme, title: text, label: value, isFinal: isFinal, hasSeparator: hasSeparator, shimmeringIndex: nil, sectionId: self.section) case let .paymentMethod(_, text, value): @@ -284,7 +286,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - let arguments = BotReceiptControllerArguments(account: context.account) + let arguments = BotReceiptControllerArguments(account: context.account, source: .message(messageId)) let signal: Signal<(ItemListPresentationData, (ItemListNodeState, Any)), NoError> = combineLatest( context.sharedContext.presentationData, diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 1c5b8d363f7..0cab716179f 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -458,7 +458,7 @@ private final class BrowserScreenNode: ViewControllerTracingNode { strongSelf.interaction?.updateForceSerif(false) action(.default) } - })), .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(Font.with(size: 17.0, design: .serif, traits: [])), icon: forceSerif ? checkIcon : emptyIcon, action: { [weak self] (controller, action) in + })), .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceSerif ? checkIcon : emptyIcon, action: { [weak self] (controller, action) in if let strongSelf = self { strongSelf.interaction?.updateForceSerif(true) action(.default) diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index bcd23d8caac..c29f646cc41 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -308,7 +308,7 @@ public final class CallListController: TelegramBaseController { var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) diff --git a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift index cd58019495d..595a46676bf 100644 --- a/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift +++ b/submodules/ChatImportUI/Sources/ChatImportActivityScreen.swift @@ -442,7 +442,7 @@ public final class ChatImportActivityScreen: ViewController { let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])]) - let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black) + let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 2b3a2e87ae8..4b488a6276f 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -178,6 +178,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch isForum = true } + var hasRemoveFromFolder = false if case let .chatList(currentFilter) = source { if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in @@ -201,119 +202,122 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }) }) }))) - } else { - var hasFolders = false + hasRemoveFromFolder = true + } + } + + if !hasRemoveFromFolder && peerGroup != nil { + var hasFolders = false - for case let .filter(_, _, _, data) in filters { - let predicate = chatListFilterPredicate(filter: data) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue - } + for case let .filter(_, _, _, data) in filters { + let predicate = chatListFilterPredicate(filter: data) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } - var data = data - if data.addIncludePeer(peerId: peer.id) { - hasFolders = true - break - } + var data = data + if data.addIncludePeer(peerId: peer.id) { + hasFolders = true + break } + } - if hasFolders { - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in - var updatedItems: [ContextMenuItem] = [] + if hasFolders { + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + var updatedItems: [ContextMenuItem] = [] - for filter in filters { - if case let .filter(_, title, _, data) = filter { - let predicate = chatListFilterPredicate(filter: data) - if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue - } + for filter in filters { + if case let .filter(_, title, _, data) = filter { + let predicate = chatListFilterPredicate(filter: data) + if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue + } - var data = data - if !data.addIncludePeer(peerId: peer.id) { - continue - } + var data = data + if !data.addIncludePeer(peerId: peer.id) { + continue + } - let filterType = chatListFilterType(data) - updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in - let imageName: String - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" - } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - }, action: { c, f in - c.dismiss(completion: { - let isPremium = limitsData.0?.isPremium ?? false - let (_, limits, premiumLimits) = limitsData - - let limit = limits.maxFolderChatsCount - let premiumLimit = premiumLimits.maxFolderChatsCount - - let count = data.includePeers.peers.count - 1 - if count >= premiumLimit { - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {}) - chatListController?.push(controller) - return - } else if count >= limit && !isPremium { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - chatListController?.push(controller) - return + let filterType = chatListFilterType(data) + updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in + let imageName: String + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } + return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c.dismiss(completion: { + let isPremium = limitsData.0?.isPremium ?? false + let (_, limits, premiumLimits) = limitsData + + let limit = limits.maxFolderChatsCount + let premiumLimit = premiumLimits.maxFolderChatsCount + + let count = data.includePeers.peers.count - 1 + if count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {}) + chatListController?.push(controller) + return + } else if count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } - - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == filter.id { - if case let .filter(id, title, emoticon, data) = filter { - var updatedData = data - let _ = updatedData.addIncludePeer(peerId: peer.id) - filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) - } - break + chatListController?.push(controller) + return + } + + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + let _ = updatedData.addIncludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) } + break } - return filters - }).start() - - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }))) - } + } + return filters + }).start() + + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }))) } + } - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, action: { c, _ in - c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) - }))) - - c.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil) + updatedItems.append(.separator) + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil) }))) - } + + c.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil) + }))) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 8bf6283df7b..45aca2f810b 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2964,57 +2964,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController // Lottery - let getLotteryDataUseCase = appContext.resolveGetLotteryDataUseCase() let loadLotteryDataUseCase = appContext.resolveLoadLotteryDataUseCase() - lotteryDataSubscription = getLotteryDataUseCase.lotteryDataPublisher() - .compactMap { $0 } - .prefix(1) - .delay(for: .seconds(3), scheduler: RunLoop.main) - .sink { [weak self] lotteryData in - guard !AppCache.wasLotteryShown, !hideLottery else { return } - self?.showLotteryBanner(jackpot: lotteryData.currentDraw.jackpot) - } - if !hideLottery { loadLotteryDataUseCase.loadLotteryData(completion: { _ in }) } } - private func showLotteryBanner(jackpot: Money) { - if #available(iOS 13.0, *) { - AppCache.wasLotteryShown = true - showLotteryBannerAsToast(jackpot: jackpot) { [weak self] in - self?.showLotterySplash() - } - } - } - - @available(iOS 13.0, *) - private func showLotterySplash() { - let parentViewController = UIApplication.topViewController - - let navigation = makeDefaultNavigationController() - - let lotteryFlowFactory = LotteryFlowFactoryImpl(appContext: self.appContext) - let flow = lotteryFlowFactory.makeFlow(navigationController: navigation) - - let input = LotteryFlowInput() - - let handlers = LotteryFlowHandlers( - close: { [weak parentViewController] in - parentViewController?.dismiss(animated: true) - } - ) - - let lotteryController = flow.makeStartViewController(input: input, handlers: handlers) - - navigation.setViewControllers([lotteryController], animated: false) - navigation.modalPresentationStyle = .overFullScreen - - parentViewController?.present(navigation, animated: true) - } - public func showNicegramAssistant(deeplink: Deeplink?) { if #available(iOS 13, *) { guard didAppear else { diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index ddb971bef52..ad2d5de24b3 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -950,7 +950,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } self.scrollNode.bounds = updatedBounds } - transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + if abs(previousScrollBounds.minX - self.scrollNode.bounds.minX) > .ulpOfOne { + transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + } self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 2ff0d6c0717..ab11565466e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -930,7 +930,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo items.append(.action(ContextMenuActionItem(text: "Save Video", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - let _ = (saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: mediaReference) + let _ = (saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .other, mediaReference: mediaReference) |> deliverOnMainQueue).start(completed: { Queue.mainQueue().after(0.2) { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } @@ -1242,7 +1242,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(resourceIds, force: true, notify: true) + let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(Array(resourceIds), force: true, notify: true) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return @@ -1327,7 +1327,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.context.engine.messages.ensureMessagesAreLocallyAvailable(messages: messages.values.filter { messageIds.contains($0.id) }) - let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true)) + let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true, selectForumThreads: true)) peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in guard let strongSelf = self, let strongController = peerSelectionController else { return diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index e2f2e6a2281..c19d67c859f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -781,7 +781,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): - return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { + return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { interaction.addContact(phoneNumber) }) } @@ -2086,7 +2086,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { interaction.openUrl(url) }, openInstantPage: { [weak self] message, data in if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let pageController = InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor) + let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: .channel), anchor: anchor) self?.navigationController?.pushViewController(pageController) } }, longTap: { action, message in diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index 05272f9f131..f48406f6fd1 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -202,7 +202,7 @@ private final class VisualMediaItemNode: ASDisplayNode { if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize.cgSize - self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) + self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) self.fetchStatusDisposable.set(nil) self.statusNode.transitionToState(.none, completion: { [weak self] in @@ -212,7 +212,7 @@ private final class VisualMediaItemNode: ASDisplayNode { self.resourceStatus = nil } else if let file = media as? TelegramMediaFile, file.isVideo { mediaDimensions = file.dimensions?.cgSize - self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) self.mediaBadgeNode.isHidden = file.isAnimated diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index a46d523c458..d27fb2a59c0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -564,6 +564,8 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { self.playIcon.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) } + let hasSpoiler = self.message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) + var isRound = false var dimensions = CGSize(width: 100.0, height: 100.0) if case let .image(image) = self.media { @@ -572,7 +574,18 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { dimensions = largest.dimensions.cgSize if !self.requestedImage { self.requestedImage = true - let signal = mediaGridMessagePhoto(account: self.context.account, photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads) + let signal = mediaGridMessagePhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), blurred: hasSpoiler, synchronousLoad: synchronousLoads) + self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) + } + } + } else if case let .action(action) = self.media, case let .suggestedProfilePhoto(image) = action.action, let image = image { + isRound = true + self.playIcon.isHidden = true + if let largest = largestImageRepresentation(image.representations) { + dimensions = largest.dimensions.cgSize + if !self.requestedImage { + self.requestedImage = true + let signal = mediaGridMessagePhoto(account: self.context.account, userLocation: .peer(self.message.id.peerId), photoReference: .message(message: MessageReference(self.message._asMessage()), media: image), fullRepresentationSize: CGSize(width: 36.0, height: 36.0), synchronousLoad: synchronousLoads) self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) } } @@ -589,7 +602,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { dimensions = mediaDimensions.cgSize if !self.requestedImage { self.requestedImage = true - let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true) + let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, userLocation: .peer(self.message.id.peerId), videoReference: .message(message: MessageReference(self.message._asMessage()), media: file), synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, useMiniThumbnailIfAvailable: true, blurred: hasSpoiler) self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoads) } } @@ -1297,12 +1310,28 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { guard let strongSelf = self else { return } - let cachedPeerData = peerView.cachedData - if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo { - if let photo = maybePhoto, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { + let cachedPeerData = peerView.cachedData as? CachedUserData + var personalPhoto: TelegramMediaImage? + var profilePhoto: TelegramMediaImage? + var isKnown = false + + if let cachedPeerData = cachedPeerData { + if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto { + personalPhoto = maybePersonalPhoto + isKnown = true + } + if case let .known(maybePhoto) = cachedPeerData.photo { + profilePhoto = maybePhoto + isKnown = true + } + } + + if isKnown { + let photo = personalPhoto ?? profilePhoto + if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) if videoContent.id != strongSelf.videoContent?.id { strongSelf.videoNode?.removeFromSupernode() strongSelf.videoContent = videoContent @@ -1939,6 +1968,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } break inner } + } else if let action = media as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = action.action, let _ = image { + let fitSize = contentImageSize + contentImageSpecs.append((message, .action(action), fitSize)) } } } @@ -3170,7 +3202,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: floor((measureLayout.size.height - contentImageSize.height) / 2.0)) var validMediaIds: [EngineMedia.Id] = [] for (message, media, mediaSize) in contentImageSpecs { - guard let mediaId = media.id else { + var mediaId = media.id + if mediaId == nil, case let .action(action) = media, case let .suggestedProfilePhoto(image) = action.action { + mediaId = image?.id + } + guard let mediaId = mediaId else { continue } validMediaIds.append(mediaId) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 91470cc516b..a672e322749 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1009,12 +1009,12 @@ public final class ChatListNode: ListView { guard let strongSelf = self else { return } - if case .peers = strongSelf.mode { - if let strongSelf = self, let peerSelected = strongSelf.peerSelected { - peerSelected(peer, nil, true, true, nil) - } - return - } +// if case .peers = strongSelf.mode { +// if let strongSelf = self, let peerSelected = strongSelf.peerSelected { +// peerSelected(peer, nil, true, true, nil) +// } +// return +// } var didBeginSelecting = false var count = 0 strongSelf.updateState { [weak self] state in diff --git a/submodules/ChatPresentationInterfaceState/BUILD b/submodules/ChatPresentationInterfaceState/BUILD index a6c4d04ec5c..2462d80ce2f 100644 --- a/submodules/ChatPresentationInterfaceState/BUILD +++ b/submodules/ChatPresentationInterfaceState/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/ChatInterfaceState:ChatInterfaceState", "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/StickerPeekUI:StickerPeekUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatMediaInputNodeInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatMediaInputNodeInteraction.swift new file mode 100644 index 00000000000..62abff0ed44 --- /dev/null +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatMediaInputNodeInteraction.swift @@ -0,0 +1,61 @@ +import Foundation +import Postbox +import StickerPeekUI +import TelegramUIPreferences + +public struct ChatInterfaceStickerSettings: Equatable { + public let loopAnimatedStickers: Bool + + public init(loopAnimatedStickers: Bool) { + self.loopAnimatedStickers = loopAnimatedStickers + } + + public init(stickerSettings: StickerSettings) { + self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers + } + + public static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool { + return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers + } +} + +public enum ChatMediaInputGifMode: Equatable { + case recent + case trending + case emojiSearch(String) +} + +public final class ChatMediaInputNodeInteraction { + public let navigateToCollectionId: (ItemCollectionId) -> Void + public let navigateBackToStickers: () -> Void + public let setGifMode: (ChatMediaInputGifMode) -> Void + public let openSettings: () -> Void + public let openTrending: (ItemCollectionId?) -> Void + public let dismissTrendingPacks: ([ItemCollectionId]) -> Void + public let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void + public let openPeerSpecificSettings: () -> Void + public let dismissPeerSpecificSettings: () -> Void + public let clearRecentlyUsedStickers: () -> Void + + public var stickerSettings: ChatInterfaceStickerSettings? + public var highlightedStickerItemCollectionId: ItemCollectionId? + public var highlightedItemCollectionId: ItemCollectionId? + public var highlightedGifMode: ChatMediaInputGifMode = .recent + public var previewedStickerPackItem: StickerPreviewPeekItem? + public var appearanceTransition: CGFloat = 1.0 + public var displayStickerPlaceholder = true + public var displayStickerPackManageControls = true + + public init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { + self.navigateToCollectionId = navigateToCollectionId + self.navigateBackToStickers = navigateBackToStickers + self.setGifMode = setGifMode + self.openSettings = openSettings + self.openTrending = openTrending + self.dismissTrendingPacks = dismissTrendingPacks + self.toggleSearch = toggleSearch + self.openPeerSpecificSettings = openPeerSpecificSettings + self.dismissPeerSpecificSettings = dismissPeerSpecificSettings + self.clearRecentlyUsedStickers = clearRecentlyUsedStickers + } +} diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 227c77c35bd..029afd556d4 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -321,6 +321,19 @@ public class CheckLayer: CALayer { self.isOpaque = false } + public override init(layer: Any) { + guard let layer = layer as? CheckLayer else { + preconditionFailure() + } + + self.theme = layer.theme + self.content = layer.content + + super.init(layer: layer) + + self.isOpaque = false + } + public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) { self.theme = theme self.content = content diff --git a/submodules/CodeInputView/Sources/CodeInputView.swift b/submodules/CodeInputView/Sources/CodeInputView.swift index cc71e8f8942..74d3b38ee99 100644 --- a/submodules/CodeInputView/Sources/CodeInputView.swift +++ b/submodules/CodeInputView/Sources/CodeInputView.swift @@ -118,6 +118,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { private var itemViews: [ItemView] = [] public var updated: (() -> Void)? + public var longPressed: (() -> Void)? private var theme: Theme? private var count: Int? @@ -169,6 +170,18 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { } } + public override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))) + } + + @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + if case .ended = gestureRecognizer.state { + self.longPressed?() + } + } + private var isSucceed = false private var isFailed = false private var isResetting = false diff --git a/submodules/ComponentFlow/BUILD b/submodules/ComponentFlow/BUILD index 6c7d864ad53..92f3ebdd2a4 100644 --- a/submodules/ComponentFlow/BUILD +++ b/submodules/ComponentFlow/BUILD @@ -10,6 +10,7 @@ swift_library( "-warnings-as-errors", ], deps = [ + "//submodules/Display" ], visibility = [ "//visibility:public", diff --git a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift index d5f40e0280a..6871454b3b3 100644 --- a/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift +++ b/submodules/ComponentFlow/Source/Base/ChildComponentTransitions.swift @@ -35,10 +35,24 @@ public extension Transition.AppearWithGuide { } public extension Transition.Disappear { - static let `default` = Transition.Disappear { view, transition, completion in - transition.setAlpha(view: view, alpha: 0.0, completion: { _ in - completion() - }) + static func `default`(scale: Bool = false, alpha: Bool = true) -> Transition.Disappear { + return Transition.Disappear { view, transition, completion in + if scale { + transition.setScale(view: view, scale: 0.01, completion: { _ in + if !alpha { + completion() + } + }) + } + if alpha { + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + } + if !alpha && !scale { + completion() + } + } } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index b7601ab3db4..a6679fc7ba3 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Display #if targetEnvironment(simulator) @_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float @@ -15,105 +16,31 @@ private extension UIView { } } -@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate { - private let keyPath: String? - var completion: ((Bool) -> Void)? - - init(animation: CAAnimation, completion: ((Bool) -> Void)?) { - if let animation = animation as? CABasicAnimation { - self.keyPath = animation.keyPath - } else { - self.keyPath = nil - } - self.completion = completion - - super.init() - } - - @objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { - if let anim = anim as? CABasicAnimation { - if anim.keyPath != self.keyPath { - return - } - } - if let completion = self.completion { - completion(flag) - } - } -} - -private func makeSpringAnimation(keyPath: String) -> CASpringAnimation { - let springAnimation = CASpringAnimation(keyPath: keyPath) - springAnimation.mass = 3.0; - springAnimation.stiffness = 1000.0 - springAnimation.damping = 500.0 - springAnimation.duration = 0.5 - springAnimation.timingFunction = CAMediaTimingFunction(name: .linear) - return springAnimation -} - private extension CALayer { - func makeAnimation(from: AnyObject?, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation { + func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { + let timingFunction: String + let mediaTimingFunction: CAMediaTimingFunction? switch curve { case .spring: - let animation = makeSpringAnimation(keyPath: keyPath) - animation.fromValue = from - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = .forwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) - } - - let k = Float(UIView.animationDurationFactor) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - if !delay.isZero { - animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor - animation.fillMode = .both - } - - return animation + timingFunction = kCAMediaTimingFunctionSpring + mediaTimingFunction = nil default: - let k = Float(UIView.animationDurationFactor) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - let animation = CABasicAnimation(keyPath: keyPath) - if let from = from { - animation.fromValue = from - } - animation.toValue = to - animation.duration = duration - animation.timingFunction = curve.asTimingFunction() - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = .both - animation.speed = speed - animation.isAdditive = additive - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) - } - - if !delay.isZero { - animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor - animation.fillMode = .both - } - - return animation - } - } - - func animate(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) { - let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, duration: duration, delay: delay, curve: curve, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) - self.add(animation, forKey: additive ? nil : keyPath) + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + mediaTimingFunction = curve.asTimingFunction() + } + + self.animate( + from: from, + to: to, + keyPath: keyPath, + timingFunction: timingFunction, + duration: duration, + delay: delay, + mediaTimingFunction: mediaTimingFunction, + removeOnCompletion: removeOnCompletion, + additive: additive, + completion: completion + ) } } @@ -391,11 +318,11 @@ public struct Transition { } } - public func setAlpha(view: UIView, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { - self.setAlpha(layer: view.layer, alpha: alpha, completion: completion) + public func setAlpha(view: UIView, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.setAlpha(layer: view.layer, alpha: alpha, delay: delay, completion: completion) } - public func setAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + public func setAlpha(layer: CALayer, alpha: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if layer.opacity == Float(alpha) { completion?(true) return @@ -408,15 +335,15 @@ public struct Transition { case .curve: let previousAlpha = layer.presentation()?.opacity ?? layer.opacity layer.opacity = Float(alpha) - self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, completion: completion) + self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, delay: delay, completion: completion) } } - public func setScale(view: UIView, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { - self.setScale(layer: view.layer, scale: scale, completion: completion) + public func setScale(view: UIView, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.setScale(layer: view.layer, scale: scale, delay: delay, completion: completion) } - public func setScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let t = layer.presentation()?.transform ?? layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale == scale { @@ -435,7 +362,7 @@ public struct Transition { to: scale as NSNumber, keyPath: "transform.scale", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: false, @@ -476,19 +403,23 @@ public struct Transition { } public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { + self.setSublayerTransform(layer: view.layer, transform: transform, completion: completion) + } + + public func setSublayerTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: - view.layer.sublayerTransform = transform + layer.sublayerTransform = transform completion?(true) case let .curve(duration, curve): let previousValue: CATransform3D - if let presentation = view.layer.presentation() { + if let presentation = layer.presentation() { previousValue = presentation.sublayerTransform } else { - previousValue = view.layer.sublayerTransform + previousValue = layer.sublayerTransform } - view.layer.sublayerTransform = transform - view.layer.animate( + layer.sublayerTransform = transform + layer.animate( from: NSValue(caTransform3D: previousValue), to: NSValue(caTransform3D: transform), keyPath: "sublayerTransform", @@ -502,7 +433,7 @@ public struct Transition { } } - public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) @@ -512,7 +443,7 @@ public struct Transition { to: toValue as NSNumber, keyPath: "transform.scale", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: additive, @@ -540,11 +471,11 @@ public struct Transition { } } - public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateAlpha(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion) } - public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animateAlpha(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) @@ -554,7 +485,7 @@ public struct Transition { to: toValue as NSNumber, keyPath: "opacity", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: additive, @@ -601,7 +532,7 @@ public struct Transition { public func animateBounds(layer: CALayer, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: - break + completion?(true) case let .curve(duration, curve): layer.animate( from: NSValue(cgRect: fromValue), @@ -620,7 +551,7 @@ public struct Transition { public func animateBoundsOrigin(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: - break + completion?(true) case let .curve(duration, curve): layer.animate( from: NSValue(cgPoint: fromValue), @@ -639,7 +570,7 @@ public struct Transition { public func animateBoundsSize(layer: CALayer, from fromValue: CGSize, to toValue: CGSize, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: - break + completion?(true) case let .curve(duration, curve): layer.animate( from: NSValue(cgSize: fromValue), @@ -657,6 +588,7 @@ public struct Transition { public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { if layer.cornerRadius == cornerRadius { + completion?(true) return } switch self.animation { @@ -689,8 +621,9 @@ public struct Transition { switch self.animation { case .none: layer.path = path + completion?(true) case let .curve(duration, curve): - if let previousPath = layer.path { + if let previousPath = layer.path, previousPath != path { layer.animate( from: previousPath, to: path, @@ -705,14 +638,39 @@ public struct Transition { layer.path = path } else { layer.path = path + completion?(true) } } } + public func setShapeLayerLineWidth(layer: CAShapeLayer, lineWidth: CGFloat, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.lineWidth = lineWidth + completion?(true) + case let .curve(duration, curve): + let previousLineWidth = layer.lineWidth + layer.lineWidth = lineWidth + + layer.animate( + from: previousLineWidth as NSNumber, + to: lineWidth as NSNumber, + keyPath: "lineWidth", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setShapeLayerLineDashPattern(layer: CAShapeLayer, pattern: [NSNumber], completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: layer.lineDashPattern = pattern + completion?(true) case let .curve(duration, curve): if let previousLineDashPattern = layer.lineDashPattern { layer.lineDashPattern = pattern @@ -730,7 +688,40 @@ public struct Transition { ) } else { layer.lineDashPattern = pattern + completion?(true) } } } + + public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) { + self.setBackgroundColor(layer: view.layer, color: color, completion: completion) + } + + public func setBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let current = layer.backgroundColor, current == color.cgColor { + completion?(true) + return + } + + switch self.animation { + case .none: + layer.backgroundColor = color.cgColor + completion?(true) + case let .curve(duration, curve): + let previousColor: CGColor = layer.backgroundColor ?? UIColor.clear.cgColor + layer.backgroundColor = color.cgColor + + layer.animate( + from: previousColor, + to: color.cgColor, + keyPath: "backgroundColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } } diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 1332bd0af0b..ed1e57024ef 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -6,11 +6,13 @@ public final class Button: Component { public let minSize: CGSize? public let tag: AnyObject? public let automaticHighlight: Bool + public let isEnabled: Bool public let action: () -> Void public let holdAction: (() -> Void)? convenience public init( content: AnyComponent, + isEnabled: Bool = true, action: @escaping () -> Void ) { self.init( @@ -18,6 +20,7 @@ public final class Button: Component { minSize: nil, tag: nil, automaticHighlight: true, + isEnabled: isEnabled, action: action, holdAction: nil ) @@ -28,6 +31,7 @@ public final class Button: Component { minSize: CGSize? = nil, tag: AnyObject? = nil, automaticHighlight: Bool = true, + isEnabled: Bool = true, action: @escaping () -> Void, holdAction: (() -> Void)? ) { @@ -35,6 +39,7 @@ public final class Button: Component { self.minSize = minSize self.tag = tag self.automaticHighlight = automaticHighlight + self.isEnabled = isEnabled self.action = action self.holdAction = holdAction } @@ -45,6 +50,7 @@ public final class Button: Component { minSize: minSize, tag: self.tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: self.holdAction ) @@ -56,6 +62,7 @@ public final class Button: Component { minSize: self.minSize, tag: self.tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: holdAction ) @@ -67,6 +74,7 @@ public final class Button: Component { minSize: self.minSize, tag: tag, automaticHighlight: self.automaticHighlight, + isEnabled: self.isEnabled, action: self.action, holdAction: self.holdAction ) @@ -85,6 +93,9 @@ public final class Button: Component { if lhs.automaticHighlight != rhs.automaticHighlight { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } return true } @@ -98,11 +109,28 @@ public final class Button: Component { return } if self.currentIsHighlighted != oldValue { - self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + self.updateAlpha(transition: .immediate) } } } + private func updateAlpha(transition: Transition) { + guard let component = self.component else { + return + } + let alpha: CGFloat + if component.isEnabled { + if component.automaticHighlight { + alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } else { + alpha = 1.0 + } + } else { + alpha = 0.4 + } + transition.setAlpha(view: self.contentView, alpha: alpha) + } + private var holdActionTriggerred: Bool = false private var holdActionTimer: Timer? @@ -218,6 +246,9 @@ public final class Button: Component { self.component = component + self.updateAlpha(transition: transition) + self.isEnabled = component.isEnabled + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil) return size diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index ec1ae334f08..2c8a3d87b6c 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? { +public func findTaggedComponentViewImpl(view: UIView, tag: Any) -> UIView? { if let view = view as? ComponentTaggedView { if view.matches(tag: tag) { return view @@ -9,7 +9,7 @@ private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? { } for subview in view.subviews { - if let result = findTaggedViewImpl(view: subview, tag: tag) { + if let result = findTaggedComponentViewImpl(view: subview, tag: tag) { return result } } @@ -131,7 +131,7 @@ public final class ComponentHostView: UIView { return nil } - return findTaggedViewImpl(view: componentView, tag: tag) + return findTaggedComponentViewImpl(view: componentView, tag: tag) } } @@ -141,6 +141,7 @@ public final class ComponentView { private var currentSize: CGSize? public private(set) var view: UIView? private(set) var isUpdating: Bool = false + public weak var parentState: ComponentState? public init() { } @@ -154,6 +155,15 @@ public final class ComponentView { self.currentSize = size return size } + + public func updateEnvironment(transition: Transition, @EnvironmentBuilder environment: () -> Environment) -> CGSize? { + guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else { + return nil + } + let size = self._update(transition: transition, component: currentComponent, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: currentContainerSize) + self.currentSize = size + return size + } private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { precondition(!self.isUpdating) @@ -181,10 +191,15 @@ public final class ComponentView { context.erasedEnvironment = environmentResult } + var isStateUpdated = false + if componentState.isUpdated { + isStateUpdated = true + componentState.isUpdated = false + } + let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() - - if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { + if !forceUpdate, !isEnvironmentUpdated, !isStateUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { if currentContainerSize == containerSize && currentComponent == component { self.isUpdating = false return currentSize @@ -197,9 +212,13 @@ public final class ComponentView { guard let strongSelf = self else { return } - let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { - preconditionFailure() - } as () -> Environment, updateEnvironment: false, forceUpdate: true, containerSize: containerSize) + if let parentState = strongSelf.parentState { + parentState.updated(transition: transition) + } else { + let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: { + preconditionFailure() + } as () -> Environment, updateEnvironment: false, forceUpdate: true, containerSize: containerSize) + } } let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition) @@ -207,6 +226,9 @@ public final class ComponentView { if isEnvironmentUpdated { context.erasedEnvironment._isUpdated = false } + if isStateUpdated { + context.erasedState.isUpdated = false + } self.isUpdating = false @@ -217,7 +239,7 @@ public final class ComponentView { guard let view = self.view else { return nil } - return findTaggedViewImpl(view: view, tag: tag) + return findTaggedComponentViewImpl(view: view, tag: tag) } } diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index c584daf0e8d..91fa37a01df 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -20,10 +20,27 @@ public final class LottieAnimationComponent: Component { public var name: String public var mode: Mode + public var range: (CGFloat, CGFloat)? - public init(name: String, mode: Mode) { + public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil) { self.name = name self.mode = mode + self.range = range + } + + public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool { + if lhs.name != rhs.name { + return false + } + if lhs.mode != rhs.mode { + return false + } + if let lhsRange = lhs.range, let rhsRange = rhs.range, lhsRange != rhsRange { + return false + } else if (lhs.range == nil) != (rhs.range == nil) { + return false + } + return true } } @@ -248,8 +265,14 @@ public final class LottieAnimationComponent: Component { if updatePlayback { if case .animating = component.animation.mode { if !animationView.isAnimationPlaying { - animationView.play { [weak self] _ in - self?.currentCompletion?() + if let range = component.animation.range { + animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in + self?.currentCompletion?() + }) + } else { + animationView.play { [weak self] _ in + self?.currentCompletion?() + } } } } else { diff --git a/submodules/Components/PagerComponent/BUILD b/submodules/Components/PagerComponent/BUILD index ed1b87f808f..182a32f152d 100644 --- a/submodules/Components/PagerComponent/BUILD +++ b/submodules/Components/PagerComponent/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", + "//submodules/DirectionalPanGesture:DirectionalPanGesture", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index ebe517155ad..018c053c55b 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import DirectionalPanGesture public protocol PagerExpandableScrollView: UIScrollView { } @@ -181,10 +182,12 @@ public final class PagerComponent>? public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let bottomPanel: AnyComponent>? + public let externalBottomPanelContainer: PagerExternalTopPanelContainer? public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void public let panelHideBehavior: PagerComponentPanelHideBehavior + public let clipContentToTopPanel: Bool public init( contentInsets: UIEdgeInsets, @@ -198,10 +201,12 @@ public final class PagerComponent>?, externalTopPanelContainer: PagerExternalTopPanelContainer?, bottomPanel: AnyComponent>?, + externalBottomPanelContainer: PagerExternalTopPanelContainer?, panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?, isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void, isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void, - panelHideBehavior: PagerComponentPanelHideBehavior + panelHideBehavior: PagerComponentPanelHideBehavior, + clipContentToTopPanel: Bool ) { self.contentInsets = contentInsets self.contents = contents @@ -214,10 +219,12 @@ public final class PagerComponent Bool { @@ -248,9 +255,15 @@ public final class PagerComponent? private let topPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>() @@ -300,11 +314,17 @@ public final class PagerComponent>() self.bottomPanelView = bottomPanelView - self.addSubview(bottomPanelView) } + + let bottomPanelSuperview = component.externalBottomPanelContainer ?? self + if bottomPanelView.superview !== bottomPanelSuperview { + bottomPanelSuperview.addSubview(bottomPanelView) + } + let bottomPanelSize = bottomPanelView.update( transition: bottomPanelTransition, component: bottomPanel, @@ -610,7 +643,7 @@ public final class PagerComponent() self.contentBackgroundView = contentBackgroundView - self.insertSubview(contentBackgroundView, at: 0) + self.contentClippingView.insertSubview(contentBackgroundView, at: 0) } let _ = contentBackgroundView.update( transition: contentBackgroundTransition, @@ -625,11 +658,9 @@ public final class PagerComponent()) - contentTransition = .immediate + contentTransition = transition.withAnimation(.none) self.contentViews[content.id] = contentView if let contentBackgroundView = self.contentBackgroundView { - self.insertSubview(contentView.view, aboveSubview: contentBackgroundView) + self.contentClippingView.insertSubview(contentView.view, aboveSubview: contentBackgroundView) } else { - self.insertSubview(contentView.view, at: 0) + self.contentClippingView.insertSubview(contentView.view, at: 0) } } diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index a4ef901b9f1..35d2f8881aa 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -143,6 +143,7 @@ public final class ReactionIconView: PortalSourceView { let animationLayer = InlineStickerItemLayer( context: context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift index 413f41ec207..5f4efc9730c 100644 --- a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -19,7 +19,7 @@ public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage", public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal { return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch { return Signal { subscriber in - let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: animation.resource)).start() + let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start() let type: AnimationCacheAnimationType if animation.isVideoSticker || animation.isVideoEmoji { @@ -29,7 +29,17 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi } else { type = .still } - let fetchFrame = animationCacheFetchFile(context: context, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true) + + var customColor: UIColor? + for attribute in animation.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + customColor = nil + } + } + } + + let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor) class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { let queue: Queue diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 9a52a5229c7..88fbc0a261a 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -195,6 +195,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let reactionLayer = InlineStickerItemLayer( context: context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), file: file, @@ -437,6 +438,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let reactionLayer = InlineStickerItemLayer( context: context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), file: file, diff --git a/submodules/Components/SheetComponent/BUILD b/submodules/Components/SheetComponent/BUILD index 1b7f99b7f45..5d521914299 100644 --- a/submodules/Components/SheetComponent/BUILD +++ b/submodules/Components/SheetComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index ad0576e9aa8..e0d1b597f9f 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -3,15 +3,20 @@ import UIKit import Display import ComponentFlow import ViewControllerComponent +import SwiftSignalKit public final class SheetComponentEnvironment: Equatable { public let isDisplaying: Bool public let isCentered: Bool + public let hasInputHeight: Bool + public let regularMetricsSize: CGSize? public let dismiss: (Bool) -> Void - public init(isDisplaying: Bool, isCentered: Bool, dismiss: @escaping (Bool) -> Void) { + public init(isDisplaying: Bool, isCentered: Bool, hasInputHeight: Bool, regularMetricsSize: CGSize?, dismiss: @escaping (Bool) -> Void) { self.isDisplaying = isDisplaying self.isCentered = isCentered + self.hasInputHeight = hasInputHeight + self.regularMetricsSize = regularMetricsSize self.dismiss = dismiss } @@ -22,6 +27,12 @@ public final class SheetComponentEnvironment: Equatable { if lhs.isCentered != rhs.isCentered { return false } + if lhs.hasInputHeight != rhs.hasInputHeight { + return false + } + if lhs.regularMetricsSize != rhs.regularMetricsSize { + return false + } return true } } @@ -29,11 +40,25 @@ public final class SheetComponentEnvironment: Equatable { public final class SheetComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) + public enum BackgroundColor: Equatable { + public enum BlurStyle: Equatable { + case light + case dark + } + + case color(UIColor) + case blur(BlurStyle) + } + public let content: AnyComponent - public let backgroundColor: UIColor + public let backgroundColor: BackgroundColor public let animateOut: ActionSlot> - public init(content: AnyComponent, backgroundColor: UIColor, animateOut: ActionSlot>) { + public init( + content: AnyComponent, + backgroundColor: BackgroundColor, + animateOut: ActionSlot> + ) { self.content = content self.backgroundColor = backgroundColor self.animateOut = animateOut @@ -49,24 +74,39 @@ public final class SheetComponent: Component { if lhs.animateOut != rhs.animateOut { return false } - return true } + private class ScrollView: UIScrollView { + var ignoreScroll = false + override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { + guard !self.ignoreScroll else { + return + } + if animated && abs(contentOffset.y - self.contentOffset.y) > 200.0 { + return + } + super.setContentOffset(contentOffset, animated: animated) + } + } + public final class View: UIView, UIScrollViewDelegate { private let dimView: UIView - private let scrollView: UIScrollView + private let scrollView: ScrollView private let backgroundView: UIView + private var effectView: UIVisualEffectView? private let contentView: ComponentHostView private var previousIsDisplaying: Bool = false private var dismiss: ((Bool) -> Void)? + private var keyboardWillShowObserver: AnyObject? + override init(frame: CGRect) { self.dimView = UIView() self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4) - self.scrollView = UIScrollView() + self.scrollView = ScrollView() self.scrollView.delaysContentTouches = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never @@ -76,7 +116,7 @@ public final class SheetComponent: Component { self.scrollView.alwaysBounceVertical = true self.backgroundView = UIView() - self.backgroundView.layer.cornerRadius = 10.0 + self.backgroundView.layer.cornerRadius = 12.0 self.backgroundView.layer.masksToBounds = true self.contentView = ComponentHostView() @@ -91,12 +131,27 @@ public final class SheetComponent: Component { self.addSubview(self.scrollView) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:)))) + + self.keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { [weak self] _ in + if let strongSelf = self { + strongSelf.scrollView.ignoreScroll = true + Queue.mainQueue().after(0.1, { + strongSelf.scrollView.ignoreScroll = false + }) + } + }) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + if let keyboardFrameChangeObserver = self.keyboardWillShowObserver { + NotificationCenter.default.removeObserver(keyboardFrameChangeObserver) + } + } + @objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismiss?(true) @@ -183,8 +238,10 @@ public final class SheetComponent: Component { } } + private var currentHasInputHeight = false private var currentAvailableSize: CGSize? func update(component: SheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousHasInputHeight = self.currentHasInputHeight let sheetEnvironment = environment[SheetComponentEnvironment.self].value component.animateOut.connect { [weak self] completion in guard let strongSelf = self else { @@ -195,18 +252,36 @@ public final class SheetComponent: Component { } } - if self.backgroundView.backgroundColor != component.backgroundColor { - self.backgroundView.backgroundColor = component.backgroundColor - } + self.currentHasInputHeight = sheetEnvironment.hasInputHeight + switch component.backgroundColor { + case let .blur(style): + self.backgroundView.isHidden = true + if self.effectView == nil { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light)) + effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius + effectView.layer.masksToBounds = true + self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView) + self.effectView = effectView + } + case let .color(color): + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + self.effectView?.removeFromSuperview() + self.effectView = nil + } + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) - let containerSize: CGSize + var containerSize: CGSize if sheetEnvironment.isCentered { let verticalInset: CGFloat = 44.0 let maxSide = max(availableSize.width, availableSize.height) let minSide = min(availableSize.width, availableSize.height) containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0) + if let regularMetricsSize = sheetEnvironment.regularMetricsSize { + containerSize = regularMetricsSize + } } else { containerSize = CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) } @@ -226,9 +301,15 @@ public final class SheetComponent: Component { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) + } } else { transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + if let effectView = self.effectView { + transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) + } } transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) @@ -239,6 +320,10 @@ public final class SheetComponent: Component { if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height { self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)) } + if self.currentHasInputHeight != previousHasInputHeight { + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size)) + } + self.currentAvailableSize = availableSize if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index 1064e745205..bb83afb0817 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -9,6 +9,7 @@ public final class SolidRoundedButtonComponent: Component { public typealias Theme = SolidRoundedButtonTheme public let title: String? + public let label: String? public let icon: UIImage? public let theme: SolidRoundedButtonTheme public let font: SolidRoundedButtonFont @@ -16,6 +17,7 @@ public final class SolidRoundedButtonComponent: Component { public let height: CGFloat public let cornerRadius: CGFloat public let gloss: Bool + public let isEnabled: Bool public let iconName: String? public let animationName: String? public let iconPosition: SolidRoundedButtonIconPosition @@ -25,6 +27,7 @@ public final class SolidRoundedButtonComponent: Component { public init( title: String? = nil, + label: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, @@ -32,6 +35,7 @@ public final class SolidRoundedButtonComponent: Component { height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false, + isEnabled: Bool = true, iconName: String? = nil, animationName: String? = nil, iconPosition: SolidRoundedButtonIconPosition = .left, @@ -40,6 +44,7 @@ public final class SolidRoundedButtonComponent: Component { action: @escaping () -> Void ) { self.title = title + self.label = label self.icon = icon self.theme = theme self.font = font @@ -47,6 +52,7 @@ public final class SolidRoundedButtonComponent: Component { self.height = height self.cornerRadius = cornerRadius self.gloss = gloss + self.isEnabled = isEnabled self.iconName = iconName self.animationName = animationName self.iconPosition = iconPosition @@ -59,6 +65,9 @@ public final class SolidRoundedButtonComponent: Component { if lhs.title != rhs.title { return false } + if lhs.label != rhs.label { + return false + } if lhs.icon !== rhs.icon { return false } @@ -80,6 +89,9 @@ public final class SolidRoundedButtonComponent: Component { if lhs.gloss != rhs.gloss { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } if lhs.iconName != rhs.iconName { return false } @@ -108,6 +120,7 @@ public final class SolidRoundedButtonComponent: Component { if self.button == nil { let button = SolidRoundedButtonView( title: component.title, + label: component.label, icon: component.icon, theme: component.theme, font: component.font, @@ -127,12 +140,16 @@ public final class SolidRoundedButtonComponent: Component { if let button = self.button { button.title = component.title + button.label = component.label button.iconPosition = component.iconPosition button.iconSpacing = component.iconSpacing button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) } button.animation = component.animationName button.gloss = component.gloss + button.isEnabled = component.isEnabled + button.isUserInteractionEnabled = component.isEnabled + button.updateTheme(component.theme) let height = button.updateLayout(width: availableSize.width, transition: .immediate) transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil) diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 156f4aab637..9d29d934b23 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -27,6 +27,7 @@ open class ViewControllerComponentContainer: ViewController { public let inputHeight: CGFloat public let metrics: LayoutMetrics public let deviceMetrics: DeviceMetrics + public let orientation: UIInterfaceOrientation? public let isVisible: Bool public let theme: PresentationTheme public let strings: PresentationStrings @@ -40,6 +41,7 @@ open class ViewControllerComponentContainer: ViewController { inputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, + orientation: UIInterfaceOrientation? = nil, isVisible: Bool, theme: PresentationTheme, strings: PresentationStrings, @@ -52,6 +54,7 @@ open class ViewControllerComponentContainer: ViewController { self.inputHeight = inputHeight self.metrics = metrics self.deviceMetrics = deviceMetrics + self.orientation = orientation self.isVisible = isVisible self.theme = theme self.strings = strings @@ -82,6 +85,9 @@ open class ViewControllerComponentContainer: ViewController { if lhs.deviceMetrics != rhs.deviceMetrics { return false } + if lhs.orientation != rhs.orientation { + return false + } if lhs.isVisible != rhs.isVisible { return false } diff --git a/submodules/ContactListUI/Sources/ContactAddItem.swift b/submodules/ContactListUI/Sources/ContactAddItem.swift index ec494117f83..8af09937ad5 100644 --- a/submodules/ContactListUI/Sources/ContactAddItem.swift +++ b/submodules/ContactListUI/Sources/ContactAddItem.swift @@ -7,10 +7,12 @@ import TelegramCore import TelegramPresentationData import AppBundle import PhoneNumberFormat +import AccountContext private let titleFont = Font.regular(17.0) public class ContactsAddItem: ListViewItem { + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let phoneNumber: String @@ -18,7 +20,8 @@ public class ContactsAddItem: ListViewItem { public let header: ListViewItemHeader? - public init(theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) { + self.context = context self.theme = theme self.strings = strings self.phoneNumber = phoneNumber @@ -187,7 +190,7 @@ class ContactsAddItemNode: ListViewItemNode { let leftInset: CGFloat = 65.0 + params.leftInset let rightInset: CGFloat = 10.0 + params.rightInset - let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor) + let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(context: item.context, number: item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index c01bd6b4025..3333f44ac5f 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -124,7 +124,7 @@ private enum ContactListSearchEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?) -> ListViewItem { switch self { case let .addContact(theme, strings, phoneNumber): - return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { + return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { addContact?(phoneNumber) }) case let .peer(_, theme, strings, peer, presence, group, enabled): diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index f0860edb018..6d9b1e33e8a 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/UndoUI:UndoUI", + "//submodules/AnimationUI:AnimationUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index f9bc303629d..af50961aee8 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -89,7 +89,7 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { case .small: titleFont = smallTextFont titleBoldFont = smallBoldTextFont - case let .custom(customFont): + case let .custom(customFont, _, _): titleFont = customFont titleBoldFont = customFont } @@ -340,7 +340,7 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { titleFont = textFont case .small: titleFont = smallTextFont - case let .custom(customFont): + case let .custom(customFont, _, _): titleFont = customFont } @@ -387,7 +387,7 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { titleFont = textFont case .small: titleFont = smallTextFont - case let .custom(customFont): + case let .custom(customFont, _, _): titleFont = customFont } diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index 14365a7fb8a..b002bf3e078 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -586,7 +586,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { let textRightInset: CGFloat if let _ = self.iconNode.image { - textRightInset = iconSize.width - 8.0 + textRightInset = iconSize.width - 2.0 } else { textRightInset = 0.0 } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 58e55b3238e..dd197c7e082 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -52,7 +52,7 @@ public enum ContextMenuActionResult { public enum ContextMenuActionItemFont { case regular case small - case custom(UIFont) + case custom(font: UIFont, height: CGFloat?, verticalOffset: CGFloat?) } public struct ContextMenuActionItemIconSource { @@ -102,6 +102,7 @@ public final class ContextMenuActionItem { public let badge: ContextMenuActionBadge? public let icon: (PresentationTheme) -> UIImage? public let iconSource: ContextMenuActionItemIconSource? + public let animationName: String? public let textIcon: (PresentationTheme) -> UIImage? public let textLinkAction: () -> Void public let action: ((Action) -> Void)? @@ -116,6 +117,7 @@ public final class ContextMenuActionItem { badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, + animationName: String? = nil, textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil }, textLinkAction: @escaping () -> Void = {}, action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)? @@ -130,6 +132,7 @@ public final class ContextMenuActionItem { badge: badge, icon: icon, iconSource: iconSource, + animationName: animationName, textIcon: textIcon, textLinkAction: textLinkAction, action: action.flatMap { action in @@ -150,6 +153,7 @@ public final class ContextMenuActionItem { badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, + animationName: String? = nil, textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil }, textLinkAction: @escaping () -> Void = {}, action: ((Action) -> Void)? @@ -163,6 +167,7 @@ public final class ContextMenuActionItem { self.badge = badge self.icon = icon self.iconSource = iconSource + self.animationName = animationName self.textIcon = textIcon self.textLinkAction = textLinkAction self.action = action diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index de69fbe0ecf..158561a714c 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -12,6 +12,7 @@ import Markdown import EntityKeyboard import AnimationCache import MultiAnimationRenderer +import AnimationUI public protocol ContextControllerActionsStackItemNode: ASDisplayNode { var wantsFullWidth: Bool { get } @@ -66,6 +67,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private let badgeNode: ASImageNode private let labelNode: ImmediateTextNode private let iconNode: ASImageNode + private var animationNode: AnimationNode? private var iconDisposable: Disposable? @@ -104,7 +106,7 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin self.iconNode = ASImageNode() self.iconNode.isAccessibilityElement = false self.iconNode.isUserInteractionEnabled = false - + super.init() self.isAccessibilityElement = true @@ -137,6 +139,12 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin self.iconDisposable?.dispose() } + override func didLoad() { + super.didLoad() + + self.view.isExclusiveTouch = true + } + @objc private func pressed() { guard let controller = self.getController() else { return @@ -206,12 +214,16 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin self.titleLabelNode.lineSpacing = 0.1 } + var forcedHeight: CGFloat? + var titleVerticalOffset: CGFloat? let titleFont: UIFont let titleBoldFont: UIFont switch self.item.textFont { - case let .custom(font): + case let .custom(font, height, verticalOffset): titleFont = font titleBoldFont = font + forcedHeight = height + titleVerticalOffset = verticalOffset case .small: let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) titleFont = smallTextFont @@ -301,6 +313,14 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin } } else if let image = self.iconNode.image { iconSize = image.size + } else if let animationName = self.item.animationName { + if self.animationNode == nil { + let animationNode = AnimationNode(animation: animationName, colors: ["__allcolors__": titleColor], scale: 1.0) + animationNode.loop(count: 3) + self.addSubnode(animationNode) + self.animationNode = animationNode + } + iconSize = CGSize(width: 24.0, height: 24.0) } else { let iconImage = self.item.icon(presentationData.theme) self.iconNode.image = iconImage @@ -335,15 +355,22 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin } else { minSize.width += sideInset } - minSize.height += verticalInset * 2.0 - minSize.height += titleSize.height - if subtitle != nil { - minSize.height += titleSubtitleSpacing - minSize.height += subtitleSize.height + if let forcedHeight { + minSize.height = forcedHeight + } else { + minSize.height += verticalInset * 2.0 + minSize.height += titleSize.height + if subtitle != nil { + minSize.height += titleSubtitleSpacing + minSize.height += subtitleSize.height + } } return (minSize: minSize, apply: { size, transition in - let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize) + var titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize) + if let titleVerticalOffset { + titleFrame = titleFrame.offsetBy(dx: 0.0, dy: titleVerticalOffset) + } let subtitleFrame = CGRect(origin: CGPoint(x: sideInset, y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) let badgeFrame = CGRect(origin: CGPoint( x: titleFrame.maxX + (badgeSize.width / 2), @@ -365,6 +392,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin let iconWidth = max(standardIconWidth, iconSize.width) let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) transition.updateFrame(node: self.iconNode, frame: iconFrame, beginWithCurrentState: true) + if let animationNode = self.animationNode { + transition.updateFrame(node: animationNode, frame: iconFrame, beginWithCurrentState: true) + } } }) } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 55e7277f9f2..18984f580b9 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -770,6 +770,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let reactionContextNode = self.reactionContextNode { additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance } + if case .reference = self.source { + if actionsFrame.maxY > layout.size.height { + actionsFrame.origin.y = contentRect.minY - actionsSize.height - contentActionsSpacing + } + } if case .center = actionsHorizontalAlignment { actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { @@ -808,6 +813,11 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo actionsFrame.origin.x = actionsEdgeInset } } + + if case let .reference(reference) = self.source, let transitionInfo = reference.transitionInfo(), let customPosition = transitionInfo.customPosition { + actionsFrame = actionsFrame.offsetBy(dx: customPosition.x, dy: customPosition.y) + } + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY), beginWithCurrentState: true) if let contentNode = contentNode { @@ -1180,11 +1190,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } - if case .reference = self.source { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsStackNode.frame.midX } - let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 59655395f52..f4ca8351d03 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -88,6 +88,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { case resetDatabaseAndCache(PresentationTheme) case resetHoles(PresentationTheme) case reindexUnread(PresentationTheme) + case resetCacheIndex + case reindexCache case resetBiometricsData(PresentationTheme) case resetWebViewCache(PresentationTheme) case optimizeDatabase(PresentationTheme) @@ -102,6 +104,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case enableReactionOverrides(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) + case enableQuickReactionSwitch(Bool) case voiceConference case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) @@ -127,7 +130,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -192,40 +195,46 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 21 case .reindexUnread: return 22 - case .resetBiometricsData: + case .resetCacheIndex: return 23 - case .resetWebViewCache: + case .reindexCache: return 24 - case .optimizeDatabase: + case .resetBiometricsData: return 25 - case .photoPreview: + case .resetWebViewCache: return 26 - case .knockoutWallpaper: + case .optimizeDatabase: return 27 - case .experimentalCompatibility: + case .photoPreview: return 28 - case .enableDebugDataDisplay: + case .knockoutWallpaper: return 29 - case .acceleratedStickers: + case .experimentalCompatibility: return 30 - case .experimentalBackground: + case .enableDebugDataDisplay: return 31 - case .inlineForums: + case .acceleratedStickers: return 32 - case .localTranscription: + case .experimentalBackground: return 33 - case .enableReactionOverrides: + case .inlineForums: return 34 - case .restorePurchases: + case .localTranscription: return 35 - case .playerEmbedding: + case .enableReactionOverrides: return 36 - case .playlistPlayback: + case .restorePurchases: return 37 - case .voiceConference: + case .playerEmbedding: return 38 + case .playlistPlayback: + return 39 + case .enableQuickReactionSwitch: + return 40 + case .voiceConference: + return 41 case let .preferredVideoCodec(index, _, _, _): - return 39 + index + return 42 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -1022,6 +1031,54 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) + case .resetCacheIndex: + return ItemListActionItem(presentationData: presentationData, title: "Reset Cache Index [!]", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + + context.account.postbox.mediaBox.storageBox.reset() + }) + case .reindexCache: + return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + + var signal = context.engine.resources.reindexCacheInBackground(lowImpact: false) + + var cancelImpl: (() -> Void)? + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + arguments.presentController(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let reindexDisposable = MetaDisposable() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + reindexDisposable.set(nil) + } + reindexDisposable.set((signal + |> deliverOnMainQueue).start(completed: { + })) + }) case .resetBiometricsData: return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in @@ -1162,6 +1219,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .enableQuickReactionSwitch(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Enable Quick Reaction", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.disableQuickReaction = !value + return PreferencesEntry(settings) + }) + }).start() + }) case .voiceConference: return ItemListDisclosureItem(presentationData: presentationData, title: "Voice Conference (Test)", label: "", sectionId: self.section, style: .blocks, action: { guard let _ = arguments.context else { @@ -1285,6 +1352,8 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetHoles(presentationData.theme)) if isMainApp { entries.append(.reindexUnread(presentationData.theme)) + entries.append(.resetCacheIndex) + entries.append(.reindexCache) entries.append(.resetWebViewCache(presentationData.theme)) } entries.append(.optimizeDatabase(presentationData.theme)) @@ -1302,6 +1371,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.restorePurchases(presentationData.theme)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) + entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) } let codecs: [(String, String?)] = [ diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 57ee73149c6..e27b4c49ce4 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -10,7 +10,47 @@ import MozjpegBinding import Accelerate import ManagedFile -private func generateBlurredThumbnail(image: UIImage) -> UIImage? { +private func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) { + var buffer = vImage_Buffer() + buffer.data = context.bytes + buffer.width = UInt(context.size.width * context.scale) + buffer.height = UInt(context.size.height * context.scale) + buffer.rowBytes = context.bytesPerRow + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation = saturation + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) +} + + +private func generateBlurredThumbnail(image: UIImage, adjustSaturation: Bool = false) -> UIImage? { let thumbnailContextSize = CGSize(width: 32.0, height: 32.0) guard let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) else { return nil @@ -24,6 +64,10 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? { } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if adjustSaturation { + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + return thumbnailContext.generateImage() } @@ -158,10 +202,12 @@ private func loadImage(data: Data) -> UIImage? { public final class DirectMediaImageCache { public final class GetMediaResult { public let image: UIImage? + public let blurredImage: UIImage? public let loadSignal: Signal? - init(image: UIImage?, loadSignal: Signal?) { + init(image: UIImage?, blurredImage: UIImage? = nil, loadSignal: Signal?) { self.image = image + self.blurredImage = blurredImage self.loadSignal = loadSignal } } @@ -188,12 +234,14 @@ public final class DirectMediaImageCache { return self.account.postbox.mediaBox.cachedRepresentationPathForId(resourceId.stringRepresentation, representationId: representationId, keepDuration: .general) } - private func getLoadSignal(width: Int, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal? { + private func getLoadSignal(width: Int, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, resourceSizeLimit: Int64) -> Signal? { return Signal { subscriber in let cachePath = self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)) let fetch = fetchedMediaResource( mediaBox: self.account.postbox.mediaBox, + userLocation: userLocation, + userContentType: userContentType, reference: resource, ranges: [(0 ..< resourceSizeLimit, .default)], statsCategory: .image, @@ -282,7 +330,7 @@ public final class DirectMediaImageCache { return self.getProgressiveSize(mediaReference: MediaReference.message(message: MessageReference(message), media: file).abstract, width: width, representations: file.previewRepresentations) } - private func getImageSynchronous(message: Message, media: Media, width: Int, possibleWidths: [Int]) -> GetMediaResult? { + private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? { var immediateThumbnailData: Data? var resource: (resource: MediaResourceReference, size: Int64)? if let image = media as? TelegramMediaImage { @@ -296,39 +344,55 @@ public final class DirectMediaImageCache { guard let resource = resource else { return nil } - + + var blurredImage: UIImage? + if includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } + + var resultImage: UIImage? for otherWidth in possibleWidths.reversed() { if otherWidth == width { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { - return GetMediaResult(image: image, loadSignal: nil) + return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil) } } else { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { - blurredImage = image + resultImage = image } } } - if blurredImage == nil { + if resultImage == nil { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { - blurredImage = image + resultImage = image } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { if let blurredImageValue = generateBlurredThumbnail(image: image) { - blurredImage = blurredImageValue + resultImage = blurredImageValue } } } - - return GetMediaResult(image: blurredImage, loadSignal: self.getLoadSignal(width: width, resource: resource.resource, resourceSizeLimit: resource.size)) + + return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size)) } - public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], synchronous: Bool) -> GetMediaResult? { + public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? { if synchronous { - return self.getImageSynchronous(message: message, media: media, width: width, possibleWidths: possibleWidths) + return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) } else { - return GetMediaResult(image: nil, loadSignal: Signal { subscriber in - let result = self.getImageSynchronous(message: message, media: media, width: width, possibleWidths: possibleWidths) + var immediateThumbnailData: Data? + if let image = media as? TelegramMediaImage { + immediateThumbnailData = image.immediateThumbnailData + } else if let file = media as? TelegramMediaFile { + immediateThumbnailData = file.immediateThumbnailData + } + var blurredImage: UIImage? + if includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } + return GetMediaResult(image: nil, blurredImage: blurredImage, loadSignal: Signal { subscriber in + let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) guard let result = result else { subscriber.putNext(nil) subscriber.putCompletion() diff --git a/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift b/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift index 05bc05f350f..30d493a77f7 100644 --- a/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift +++ b/submodules/DirectionalPanGesture/Sources/DirectionalPanGestureRecognizer.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -public class DirectionalPanGestureRecognizer: UIPanGestureRecognizer { +open class DirectionalPanGestureRecognizer: UIPanGestureRecognizer { public enum Direction { case horizontal case vertical diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 77f1390c67a..237cc808928 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -34,6 +34,55 @@ private let completionKey = "CAAnimationUtils_completion" public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve" +private final class FrameRangeContext { + private var animationCount: Int = 0 + private var displayLink: CADisplayLink? + + init() { + } + + func add() { + self.animationCount += 1 + self.update() + } + + func remove() { + self.animationCount -= 1 + if self.animationCount < 0 { + self.animationCount = 0 + assertionFailure() + } + self.update() + } + + @objc func displayEvent() { + } + + private func update() { + if self.animationCount != 0 { + if self.displayLink == nil { + let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent)) + + if #available(iOS 15.0, *) { + let maxFps = Float(UIScreen.main.maximumFramesPerSecond) + if maxFps > 61.0 { + displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps) + } + } + + self.displayLink = displayLink + displayLink.add(to: .main, forMode: .common) + displayLink.isPaused = false + } + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } +} + +private let frameRangeContext = FrameRangeContext() + public extension CAAnimation { var completion: ((Bool) -> Void)? { get { @@ -54,9 +103,18 @@ public extension CAAnimation { private func adjustFrameRate(animation: CAAnimation) { if #available(iOS 15.0, *) { + if let animation = animation as? CABasicAnimation { + if animation.keyPath == "opacity" { + return + } + } let maxFps = Float(UIScreen.main.maximumFramesPerSecond) if maxFps > 61.0 { - animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps) + #if DEBUG + //let _ = frameRangeContext.add() + #endif + + animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps) } } } @@ -97,32 +155,64 @@ public extension CALayer { return animation } else if timingFunction == kCAMediaTimingFunctionSpring { - let animation = makeSpringAnimation(keyPath) - animation.fromValue = from - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = .forwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) - } - - let k = Float(UIView.animationDurationFactor()) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - if !delay.isZero { - animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() - animation.fillMode = .both + if duration == 0.5 { + let animation = makeSpringAnimation(keyPath) + animation.fromValue = from + animation.toValue = to + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = .forwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + animation.isAdditive = additive + + if !delay.isZero { + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() + animation.fillMode = .both + } + + adjustFrameRate(animation: animation) + + return animation + } else { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CABasicAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.duration = duration + + animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.380, 0.700, 0.125, 1.000) + + animation.isRemovedOnCompletion = removeOnCompletion + animation.fillMode = .forwards + animation.speed = speed + animation.isAdditive = additive + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) + } + + if !delay.isZero { + animation.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) + delay * UIView.animationDurationFactor() + animation.fillMode = .both + } + + adjustFrameRate(animation: animation) + + return animation } - - adjustFrameRate(animation: animation) - - return animation } else { let k = Float(UIView.animationDurationFactor()) var speed: Float = 1.0 @@ -137,7 +227,13 @@ public extension CALayer { if let mediaTimingFunction = mediaTimingFunction { animation.timingFunction = mediaTimingFunction } else { - animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName(rawValue: timingFunction)) + switch timingFunction { + case CAMediaTimingFunctionName.linear.rawValue, CAMediaTimingFunctionName.easeIn.rawValue, CAMediaTimingFunctionName.easeOut.rawValue, CAMediaTimingFunctionName.easeInEaseOut.rawValue, CAMediaTimingFunctionName.default.rawValue: + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName(rawValue: timingFunction)) + default: + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + } + } animation.isRemovedOnCompletion = removeOnCompletion animation.fillMode = .forwards @@ -176,6 +272,8 @@ public extension CALayer { animationGroup.delegate = CALayerAnimationDelegate(animation: animationGroup, completion: completion) } + adjustFrameRate(animation: animationGroup) + self.add(animationGroup, forKey: key) } diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 0c1700570d0..a64d42a57e8 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -47,6 +47,13 @@ private func cancelOtherGestures(gesture: ContextGesture, view: UIView) { recognizer.cancel() } else if let recognizer = recognizer as? ListViewTapGestureRecognizer { recognizer.cancel() + } else if let recognizer = recognizer as? UITapGestureRecognizer { + switch recognizer.state { + case .possible: + recognizer.state = .failed + default: + break + } } } } diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index ff3239a4b41..4eb0d836bff 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -81,16 +81,12 @@ public final class ConstantDisplayLinkAnimator { guard let displayLink = self.displayLink else { return } - if #available(iOS 10.0, *) { - let preferredFramesPerSecond: Int - if self.frameInterval == 1 { - preferredFramesPerSecond = 60 - } else { - preferredFramesPerSecond = 30 + if self.frameInterval == 1 { + if #available(iOS 15.0, *) { + self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0) } - displayLink.preferredFramesPerSecond = preferredFramesPerSecond } else { - displayLink.frameInterval = self.frameInterval + displayLink.preferredFramesPerSecond = 30 } } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index f514c9866c5..b9621e57984 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -212,11 +212,13 @@ public final class TextNodeLayout: NSObject { public let range: NSRange public let rect: CGRect public let value: AnyHashable + public let textColor: UIColor - public init(range: NSRange, rect: CGRect, value: AnyHashable) { + public init(range: NSRange, rect: CGRect, value: AnyHashable, textColor: UIColor) { self.range = range self.rect = rect self.value = value + self.textColor = textColor } public static func ==(lhs: EmbeddedItem, rhs: EmbeddedItem) -> Bool { @@ -229,6 +231,9 @@ public final class TextNodeLayout: NSObject { if lhs.value != rhs.value { return false } + if lhs.textColor != rhs.textColor { + return false + } return true } } @@ -301,7 +306,18 @@ public final class TextNodeLayout: NSObject { spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) }) for embeddedItem in line.embeddedItems { - embeddedItems.append(TextNodeLayout.EmbeddedItem(range: embeddedItem.range, rect: embeddedItem.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY), value: embeddedItem.item)) + var textColor: UIColor? + if let attributedString = attributedString, embeddedItem.range.location < attributedString.length { + if let color = attributedString.attribute(.foregroundColor, at: embeddedItem.range.location, effectiveRange: nil) as? UIColor { + textColor = color + } + if textColor == nil { + if let color = attributedString.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor { + textColor = color + } + } + } + embeddedItems.append(TextNodeLayout.EmbeddedItem(range: embeddedItem.range, rect: embeddedItem.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY), value: embeddedItem.item, textColor: textColor ?? .black)) } } self.hasRTL = hasRTL diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD new file mode 100644 index 00000000000..6f8d77266d4 --- /dev/null +++ b/submodules/DrawingUI/BUILD @@ -0,0 +1,99 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "DrawingUIMetalResources", + srcs = glob([ + "MetalResources/**/*.*", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "DrawingUIBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.DrawingUI + CFBundleDevelopmentRegion + en + CFBundleName + PremiumUI + """ +) + +apple_resource_bundle( + name = "DrawingUIBundle", + infoplists = [ + ":DrawingUIBundleInfoPlist", + ], + resources = [ + ":DrawingUIMetalResources", + ], +) + +filegroup( + name = "DrawingUIResources", + srcs = glob([ + "Resources/**/*", + ], exclude = ["Resources/**/.*"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "DrawingUI", + module_name = "DrawingUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + data = [ + ":DrawingUIBundle", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/AccountContext:AccountContext", + "//submodules/LegacyUI:LegacyUI", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/SegmentedControlNode:SegmentedControlNode", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/HexColor:HexColor", + "//submodules/ContextUI:ContextUI", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/SheetComponent:SheetComponent", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/StickerResources:StickerResources", + "//submodules/ImageBlur:ImageBlur", + "//submodules/TextFormat:TextFormat", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode", + "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", + "//submodules/TelegramNotices:TelegramNotices", + "//submodules/FastBlur:FastBlur", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/DrawingUI/MetalResources/Drawing.metal b/submodules/DrawingUI/MetalResources/Drawing.metal new file mode 100644 index 00000000000..7b8f6ca1e63 --- /dev/null +++ b/submodules/DrawingUI/MetalResources/Drawing.metal @@ -0,0 +1,63 @@ +#include +using namespace metal; + +struct Vertex { + float4 position [[position]]; + float2 tex_coord; +}; + +struct Uniforms { + float4x4 scaleMatrix; +}; + +struct Point { + float4 position [[position]]; + float4 color; + float angle; + float size [[point_size]]; +}; + +vertex Vertex vertex_render_target(constant Vertex *vertexes [[ buffer(0) ]], + constant Uniforms &uniforms [[ buffer(1) ]], + uint vid [[vertex_id]]) +{ + Vertex out = vertexes[vid]; + out.position = uniforms.scaleMatrix * out.position; + return out; +}; + +fragment float4 fragment_render_target(Vertex vertex_data [[ stage_in ]], + texture2d tex2d [[ texture(0) ]]) +{ + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float4 color = float4(tex2d.sample(textureSampler, vertex_data.tex_coord)); + return color; +}; + +float2 transformPointCoord(float2 pointCoord, float a, float2 anchor) { + float2 point20 = pointCoord - anchor; + float x = point20.x * cos(a) - point20.y * sin(a); + float y = point20.x * sin(a) + point20.y * cos(a); + return float2(x, y) + anchor; +} + +vertex Point vertex_point_func(constant Point *points [[ buffer(0) ]], + constant Uniforms &uniforms [[ buffer(1) ]], + uint vid [[ vertex_id ]]) +{ + Point out = points[vid]; + float2 pos = float2(out.position.x, out.position.y); + out.position = uniforms.scaleMatrix * float4(pos, 0, 1); + out.size = out.size; + return out; +}; + +fragment float4 fragment_point_func(Point point_data [[ stage_in ]], + texture2d tex2d [[ texture(0) ]], + float2 pointCoord [[ point_coord ]]) +{ + constexpr sampler textureSampler(mag_filter::linear, min_filter::linear); + float2 tex_coord = transformPointCoord(pointCoord, point_data.angle, float2(0.5)); + float4 color = float4(tex2d.sample(textureSampler, tex_coord)); + return float4(point_data.color.rgb, color.a * point_data.color.a); +}; diff --git a/submodules/DrawingUI/Resources/marker.png b/submodules/DrawingUI/Resources/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..b6e2771a2f3a2c67bf7a666b319bcea8d3cc1653 GIT binary patch literal 2538 zcmbVO2~-nj9uENou?imCTF0nJv`Ho;9EpGj0R#=u5CzdfCzFB191|vk;Shzc1+>!F z2D$|eib&TLh$spQN`O{l>k$hIJc|Oa#YL3E>Qiu0yA!V0?sl){y_xyGncw&O|9}7M z(K=v+HDI}(XBH*6D6Ks-V7Wjl+w$Brzz^EJJWWgd|Co0hEzTO%y;X4xq$%^Fh8U2$4uP zrDI5RdV~;8m%$t{WnCcPpUNc&6bKFhsS3GL%}ouUOz?7vwfQxJ0!%<~Spa2&*&qshBTnbFo0T+VJ^&LQg&51?Cl&RO!KOBKcT8#uDNdH5+Ct+$d5!XN%vLT6x z*8fmY48nbexfdsuF#cKO#J6Ad?KGSi2Il3=7$P<^gA7p;(+eZU)X6JzorvjoG>o@F zD9Iiwjzryk9j@-%DcBiQBMPNyN&KkteOd3O4{An2CE0afzI(9DTD#eD#jHVNlzta= z-aX6QHR(g=tuY7kBa1nhroJ!Z8&}h}KAfaLF`n!3)Zl05$NrQ&FaJbV&`+Md-%oGt zHACV+qKqxy2$exWS!-l#_EEDb@4s5 zR?^|5=2P*GXm;gyW9@x=^+O?Dw5J!Cf2&E`9kbr_YH_Nq|29$N`46^S^NHP=kZ_Eo zD=E=uugrL2@}%hLm+LQH>}@>}x;4w@?}cqNOZ>r-5uaxJX4F`8Y{WL|$77bazZeV; z*6q4<=eVpCwQB!(VRL)CV`aNtuSJH&&wAGUd$F?07Zo?!)7~C-7_Kd;d|FD5C-rO_ z#lj6S&k~~F+k1Vl^Xk>DhP1lUSO>teYFlN4<#AVMQdO<`<=W`EKdypz+*~!f^!=S^ zc0U-C0*E@W^V|bTd-_uPhFErLSoR)P;&bB>a z^^%)i;T8R_-x=FQDJ5pE#jL{x9!5!tn!5k9#?icQ@5i|Zmup>O_*83|LZ}UL)CnqV zf_t5k;Z@cC$y9%0aIpSarrqTVCq3C% z5W>^NT^{WY4F~QOYVi-8U$`=j1%(1spC8_Ks`Rm!R-g@ub$m!&>mS$SvhYQht(?+LW0e6752gPI@T za<^>M>eTV%*FKFf(1X^*Cs7p^dRc8|+Em5+4i+^Ra9yobVs zk>{P8N18XoH|y-O1V>)m?m01fvU}WRW9-fT=lOBSYxh36kH;;;0`M>L^_?jlE_WV0 zU$t3w=q#sy31F(%yOH|upNX$_zVQ(IoA=!l z2dC}~%w+#Kl={{21zV_FeZgBB`woz!9f$SBFBZ9r4w1TDSjO_&I**3O9ZgYQKSvsi zzPw+;*ou^pc?F}L1>-|~1`ku5;Igv2(PFQ);f_do zCG+aemFW>whKX7{62_9ZJYXk$mmqC&JX5y97(SfSRou{U<;d)TgYLaUg|8dQaguDC zujp~RnieEP0ZM=EmPI{q?llb<-~bSF+) CAAction? { + return nullAction + } +} + +private struct ColorSelectionImage: Equatable { + var _image: UIImage? + let size: CGSize + let topLeftRadius: CGFloat + let topRightRadius: CGFloat + let bottomLeftRadius: CGFloat + let bottomRightRadius: CGFloat + let isLight: Bool + + init(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat, isLight: Bool) { + self.size = size + self.topLeftRadius = topLeftRadius + self.topRightRadius = topRightRadius + self.bottomLeftRadius = bottomLeftRadius + self.bottomRightRadius = bottomRightRadius + self.isLight = isLight + } + + public static func ==(lhs: ColorSelectionImage, rhs: ColorSelectionImage) -> Bool { + if lhs.size != rhs.size { + return false + } + if lhs.topLeftRadius != rhs.topLeftRadius { + return false + } + if lhs.topRightRadius != rhs.topRightRadius { + return false + } + if lhs.bottomLeftRadius != rhs.bottomLeftRadius { + return false + } + if lhs.bottomRightRadius != rhs.bottomRightRadius { + return false + } + if lhs.isLight != rhs.isLight { + return false + } + return true + } + + mutating func getImage() -> UIImage { + if self._image == nil { + self._image = generateColorSelectionImage(size: self.size, topLeftRadius: self.topLeftRadius, topRightRadius: self.topRightRadius, bottomLeftRadius: self.bottomLeftRadius, bottomRightRadius: self.bottomRightRadius, isLight: self.isLight) + } + return self._image! + } +} + +private func generateColorSelectionImage(size: CGSize, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat, isLight: Bool) -> UIImage? { + let margin: CGFloat = 10.0 + let realSize = size + + let image = generateImage(CGSize(width: size.width + margin * 2.0, height: size.height + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + let path = UIBezierPath(roundRect: CGRect(origin: CGPoint(x: margin, y: margin), size: realSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius) + context.addPath(path.cgPath) + + context.setShadow(offset: CGSize(), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.15).cgColor) + context.setLineWidth(3.0 - UIScreenPixel) + context.setStrokeColor(UIColor(rgb: isLight ? 0xffffff : 0x1a1a1c).cgColor) + context.strokePath() + }) + return image +} + +private func generateColorGridImage(size: CGSize) -> UIImage? { + return generateImage(size, opaque: true, rotatedContext: { size, context in + let squareSize = floorToScreenPixels(size.width / 12.0) + var index = 0 + for row in 0 ..< 10 { + for col in 0 ..< 12 { + let color = palleteColors[index] + var correctedSize = squareSize + if col == 11 { + correctedSize = size.width - squareSize * 11.0 + } + let rect = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: correctedSize, height: squareSize)) + + context.setFillColor(UIColor(rgb: color).cgColor) + context.fill(rect) + + index += 1 + } + } + }) +} + +private func generateCheckeredImage(size: CGSize, whiteColor: UIColor, blackColor: UIColor, length: CGFloat) -> UIImage? { + return generateImage(size, opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + let w = Int(ceil(size.width / length)) + let h = Int(ceil(size.height / length)) + for i in 0 ..< w { + for j in 0 ..< h { + if (i % 2) != (j % 2) { + context.setFillColor(whiteColor.cgColor) + } else { + context.setFillColor(blackColor.cgColor) + } + context.fill(CGRect(origin: CGPoint(x: CGFloat(i) * length, y: CGFloat(j) * length), size: CGSize(width: length, height: length))) + } + } + }) +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 32.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor(rgb: 0x1a1a1c).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image +} + +private class ColorSliderComponent: Component { + let leftColor: DrawingColor + let rightColor: DrawingColor + let currentColor: DrawingColor + let value: CGFloat + let updated: (CGFloat) -> Void + + public init( + leftColor: DrawingColor, + rightColor: DrawingColor, + currentColor: DrawingColor, + value: CGFloat, + updated: @escaping (CGFloat) -> Void + ) { + self.leftColor = leftColor + self.rightColor = rightColor + self.currentColor = currentColor + self.value = value + self.updated = updated + } + + public static func ==(lhs: ColorSliderComponent, rhs: ColorSliderComponent) -> Bool { + if lhs.leftColor != rhs.leftColor { + return false + } + if lhs.rightColor != rhs.rightColor { + return false + } + if lhs.currentColor != rhs.currentColor { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + + private let wrapper = UIView(frame: CGRect()) + private let transparencyLayer = SimpleLayer() + private let gradientLayer = GradientLayer() + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + fileprivate var updated: (CGFloat) -> Void = { _ in } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let side: CGFloat = 36.0 + let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) + guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) + self.updated(value) + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let side: CGFloat = 36.0 + let location = gestureRecognizer.location(in: self).offsetBy(dx: -side * 0.5, dy: 0.0) + guard self.frame.width > 0.0 else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - side))) + self.updated(value) + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, leftColor: DrawingColor, rightColor: DrawingColor, currentColor: DrawingColor, value: CGFloat) -> CGSize { + let previousSize = self.validSize + + let sliderSize = CGSize(width: size.width, height: 36.0) + + self.validSize = sliderSize + + self.gradientLayer.type = .axial + self.gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) + self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) + self.gradientLayer.colors = [leftColor.toUIColor().cgColor, rightColor.toUIColor().cgColor] + + if leftColor.alpha < 1.0 || rightColor.alpha < 1.0 { + self.transparencyLayer.isHidden = false + } else { + self.transparencyLayer.isHidden = true + } + + if previousSize != sliderSize { + self.wrapper.frame = CGRect(origin: .zero, size: sliderSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.transparencyLayer.frame = CGRect(origin: .zero, size: sliderSize) + if self.transparencyLayer.superlayer == nil { + self.wrapper.layer.addSublayer(self.transparencyLayer) + } + + self.gradientLayer.frame = CGRect(origin: .zero, size: sliderSize) + if self.gradientLayer.superlayer == nil { + self.wrapper.layer.addSublayer(self.gradientLayer) + } + + if self.knob.superlayer == nil { + self.layer.addSublayer(self.knob) + } + + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + } + + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 18.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + + if !self.transparencyLayer.isHidden { + self.transparencyLayer.contents = generateCheckeredImage(size: sliderSize, whiteColor: UIColor(rgb: 0xffffff, alpha: 1.0), blackColor: .clear, length: 12.0)?.cgImage + } + + self.knob.contents = generateKnobImage()?.cgImage + } + } + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: 2.0 + floorToScreenPixels((sliderSize.width - 4.0 - knobSize.width) * value), y: 2.0), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = currentColor.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + + return sliderSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + return view.updateLayout(size: availableSize, leftColor: self.leftColor, rightColor: self.rightColor, currentColor: self.currentColor, value: self.value) + } +} + +private class ColorFieldComponent: Component { + enum FieldType { + case number + case text + } + let backgroundColor: UIColor + let textColor: UIColor + let type: FieldType + let value: String + let suffix: String? + let updated: (String) -> Void + let shouldUpdate: (String) -> Bool + + public init( + backgroundColor: UIColor, + textColor: UIColor, + type: FieldType, + value: String, + suffix: String? = nil, + updated: @escaping (String) -> Void, + shouldUpdate: @escaping (String) -> Bool + ) { + self.backgroundColor = backgroundColor + self.textColor = textColor + self.type = type + self.value = value + self.suffix = suffix + self.updated = updated + self.shouldUpdate = shouldUpdate + } + + public static func ==(lhs: ColorFieldComponent, rhs: ColorFieldComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.suffix != rhs.suffix { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private var validSize: CGSize? + + private let backgroundNode = NavigationBackgroundNode(color: .clear) + private let textField = UITextField(frame: CGRect()) + private let suffixLabel = UITextField(frame: CGRect()) + + fileprivate var updated: (String) -> Void = { _ in } + fileprivate var shouldUpdate: (String) -> Bool = { _ in return true } + + func updateLayout(size: CGSize, component: ColorFieldComponent) -> CGSize { + let previousSize = self.validSize + + self.updated = component.updated + self.shouldUpdate = component.shouldUpdate + + self.validSize = size + + self.backgroundNode.frame = CGRect(origin: .zero, size: size) + self.backgroundNode.update(size: size, cornerRadius: 9.0, transition: .immediate) + self.backgroundNode.updateColor(color: component.backgroundColor, transition: .immediate) + + if previousSize == nil { + self.insertSubview(self.backgroundNode.view, at: 0) + self.addSubview(self.textField) + + self.textField.textAlignment = component.suffix != nil ? .right : .center + self.textField.delegate = self + self.textField.font = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: .monospacedNumbers) + self.textField.addTarget(self, action: #selector(self.textDidChange(_:)), for: .editingChanged) + self.textField.keyboardAppearance = .dark + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .allCharacters + + switch component.type { + case .number: + self.textField.keyboardType = .numberPad + case .text: + self.textField.keyboardType = .asciiCapable + } + } + + self.textField.textColor = component.textColor + + var textFieldOffset: CGFloat = 0.0 + if let suffix = component.suffix { + if self.suffixLabel.superview == nil { + self.suffixLabel.isUserInteractionEnabled = false + self.suffixLabel.text = suffix + self.suffixLabel.font = self.textField.font + self.suffixLabel.textColor = self.textField.textColor + self.addSubview(self.suffixLabel) + + self.suffixLabel.sizeToFit() + self.suffixLabel.frame = CGRect(origin: CGPoint(x: size.width - self.suffixLabel.frame.width - 14.0, y: floorToScreenPixels((size.height - self.suffixLabel.frame.size.height) / 2.0)), size: self.suffixLabel.frame.size) + } + textFieldOffset = -33.0 + } else { + self.suffixLabel.removeFromSuperview() + } + + self.textField.frame = CGRect(origin: CGPoint(x: textFieldOffset, y: 0.0), size: size) + self.textField.text = component.value + + return size + } + + @objc private func textDidChange(_ textField: UITextField) { + self.updated(textField.text ?? "") + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var updated = textField.text ?? "" + updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + if self.shouldUpdate(updated) { + return true + } else { + return false + } + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.selectAll(nil) + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + return view.updateLayout(size: availableSize, component: self) + } +} + +private func generatePreviewBackgroundImage(size: CGSize) -> UIImage? { + return generateImage(size, opaque: true, rotatedContext: { size, context in + context.move(to: .zero) + context.addLine(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + + context.setFillColor(UIColor.black.cgColor) + context.fillPath() + + context.move(to: CGPoint(x: size.width, y: 0.0)) + context.addLine(to: CGPoint(x: size.width, y: size.height)) + context.addLine(to: CGPoint(x: 0.0, y: size.height)) + context.closePath() + + context.setFillColor(UIColor.white.cgColor) + context.fillPath() + }) +} + +private class ColorPreviewComponent: Component { + let color: DrawingColor + + public init( + color: DrawingColor + ) { + self.color = color + } + + public static func ==(lhs: ColorPreviewComponent, rhs: ColorPreviewComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView { + private var validSize: CGSize? + + private let wrapper = UIView(frame: CGRect()) + private let background = SimpleLayer() + private let color = SimpleLayer() + + func updateLayout(size: CGSize, color: DrawingColor) -> CGSize { + let previousSize = self.validSize + + self.validSize = size + + if previousSize != size { + self.wrapper.frame = CGRect(origin: .zero, size: size) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.background.frame = CGRect(origin: .zero, size: size) + if self.background.superlayer == nil { + self.wrapper.layer.addSublayer(self.background) + } + + self.color.frame = CGRect(origin: .zero, size: size) + if self.color.superlayer == nil { + self.wrapper.layer.addSublayer(self.color) + } + + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 12.0 + if #available(iOS 13.0, *) { + self.wrapper.layer.cornerCurve = .continuous + } + } + + self.background.contents = generatePreviewBackgroundImage(size: size)?.cgImage + } + + self.color.backgroundColor = color.toUIColor().cgColor + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.updateLayout(size: availableSize, color: self.color) + } +} + +final class ColorGridComponent: Component { + let color: DrawingColor? + let selected: (DrawingColor) -> Void + + init( + color: DrawingColor?, + selected: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.selected = selected + } + + static func ==(lhs: ColorGridComponent, rhs: ColorGridComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + private var selectedColorIndex: Int? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + private var selectionKnob = UIImageView(image: nil) + private var selectionKnobImage: ColorSelectionImage? + + fileprivate var selected: (DrawingColor) -> Void = { _ in } + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height + else { + return nil + } + let row = Int(point.y / size.height * 10.0) + let col = Int(point.x / size.width * 12.0) + + let index = row * 12 + col + return DrawingColor(rgb: palleteColors[index]) + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard case .began = gestureRecognizer.state else { + return + } + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let squareSize = floorToScreenPixels(size.width / 12.0) + let imageSize = CGSize(width: size.width, height: squareSize * 10.0) + + self.validSize = imageSize + + let previousColor = self.selectedColor + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 10.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.delegate = self + pressGestureRecognizer.minimumPressDuration = 0.01 + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + self.image.image = generateColorGridImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if previousColor != selectedColor { + if let selectedColor = selectedColor { + let color = selectedColor.toUIColor().rgb + if let index = palleteColors.firstIndex(where: { $0 == color }) { + self.selectedColorIndex = index + } else { + self.selectedColorIndex = nil + } + } else { + self.selectedColorIndex = nil + } + } + + if let selectedColorIndex = self.selectedColorIndex { + if self.selectionKnob.superview == nil { + self.addSubview(self.selectionKnob) + } + + let smallCornerRadius: CGFloat = 2.0 + let largeCornerRadius: CGFloat = 10.0 + + var topLeftRadius = smallCornerRadius + var topRightRadius = smallCornerRadius + var bottomLeftRadius = smallCornerRadius + var bottomRightRadius = smallCornerRadius + + if selectedColorIndex == 0 { + topLeftRadius = largeCornerRadius + } else if selectedColorIndex == 11 { + topRightRadius = largeCornerRadius + } else if selectedColorIndex == palleteColors.count - 12 { + bottomLeftRadius = largeCornerRadius + } else if selectedColorIndex == palleteColors.count - 1 { + bottomRightRadius = largeCornerRadius + } + + let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5 ? true : false + + var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, isLight: isLight) + if selectionKnobImage != self.selectionKnobImage { + self.selectionKnob.image = selectionKnobImage.getImage() + self.selectionKnobImage = selectionKnobImage + } + + let row = Int(floor(CGFloat(selectedColorIndex) / 12.0)) + let col = selectedColorIndex % 12 + + let margin: CGFloat = 10.0 + var selectionFrame = CGRect(origin: CGPoint(x: CGFloat(col) * squareSize, y: CGFloat(row) * squareSize), size: CGSize(width: squareSize, height: squareSize)) + selectionFrame = selectionFrame.insetBy(dx: -margin, dy: -margin) + self.selectionKnob.frame = selectionFrame + } else { + self.selectionKnob.image = nil + } + + return CGSize(width: size.width, height: squareSize * 10.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.selected = self.selected + return view.updateLayout(size: availableSize, selectedColor: self.color) + } +} + +private func generateSpectrumImage(size: CGSize) -> UIImage? { + return generateImage(size, contextGenerator: { size, context in + if let image = UIImage(bundleImageName: "Media Editor/Spectrum") { + context.draw(image.cgImage!, in: CGRect(origin: .zero, size: size)) + } + if let image = UIImage(bundleImageName: "Media Editor/Grayscale") { + context.draw(image.cgImage!, in: CGRect(origin: .zero, size: size)) + } + }) +} + +final class ColorSpectrumComponent: Component { + let color: DrawingColor? + let selected: (DrawingColor) -> Void + + init( + color: DrawingColor?, + selected: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.selected = selected + } + + static func ==(lhs: ColorSpectrumComponent, rhs: ColorSpectrumComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + fileprivate var selected: (DrawingColor) -> Void = { _ in } + + private var bitmapData: UnsafeMutableRawPointer? + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height else { + return nil + } + let position = CGPoint(x: point.x / size.width, y: point.y / size.height) + let scale = self.image.image?.scale ?? 1.0 + let point = CGPoint(x: point.x * scale, y: point.y * scale) + guard let image = self.image.image?.cgImage else { + return nil + } + + var redComponent: CGFloat? + var greenComponent: CGFloat? + var blueComponent: CGFloat? + + let imageWidth = image.width + let imageHeight = image.height + + let bitmapBytesForRow = Int(imageWidth * 4) + let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) + + if self.bitmapData == nil { + let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let bitmapData = malloc(bitmapByteCount) + let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue + + let colorContext = CGContext( + data: bitmapData, + width: imageWidth, + height: imageHeight, + bitsPerComponent: 8, + bytesPerRow: bitmapBytesForRow, + space: colorSpace, + bitmapInfo: bitmapInformation + ) + + colorContext?.clear(imageRect) + colorContext?.draw(image, in: imageRect) + + self.bitmapData = bitmapData + } + + self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in + let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) + + redComponent = CGFloat(pointer[offset + 1]) / 255.0 + greenComponent = CGFloat(pointer[offset + 2]) / 255.0 + blueComponent = CGFloat(pointer[offset + 3]) / 255.0 + } + + if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { + return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) + } else { + return nil + } + } + + deinit { + if let bitmapData = self.bitmapData { + free(bitmapData) + } + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard case .began = gestureRecognizer.state else { + return + } + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.state == .changed { + let location = gestureRecognizer.location(in: self) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + } + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let imageSize = size + self.validSize = imageSize + + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.layer.allowsGroupOpacity = true + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 10.0 + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.delegate = self + pressGestureRecognizer.minimumPressDuration = 0.01 + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + if let bitmapData = self.bitmapData { + free(bitmapData) + } + self.image.image = generateSpectrumImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if let color = selectedColor, let position = color.position { + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.layer.addSublayer(self.knob) + } + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + } + + self.knob.isHidden = false + self.circle.isHidden = false + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0)), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = color.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + } else { + self.knob.isHidden = true + self.circle.isHidden = true + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.selected = self.selected + return view.updateLayout(size: availableSize, selectedColor: self.color) + } +} + +final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { + private var validSize: CGSize? + private var selectedColor: DrawingColor? + + private var wrapper = UIView(frame: CGRect()) + private var image = UIImageView(image: nil) + + private let knob = SimpleLayer() + private let circle = SimpleShapeLayer() + + private var circleMaskView = UIView() + private let maskCircle = SimpleShapeLayer() + + var selected: (DrawingColor) -> Void = { _ in } + + private var bitmapData: UnsafeMutableRawPointer? + + func getColor(at point: CGPoint) -> DrawingColor? { + guard let size = self.validSize, + point.x >= 0 && point.x <= size.width, + point.y >= 0 && point.y <= size.height else { + return nil + } + let position = CGPoint(x: point.x / size.width, y: point.y / size.height) + let scale = self.image.image?.scale ?? 1.0 + let point = CGPoint(x: point.x * scale, y: point.y * scale) + guard let image = self.image.image?.cgImage else { + return nil + } + + var redComponent: CGFloat? + var greenComponent: CGFloat? + var blueComponent: CGFloat? + + let imageWidth = image.width + let imageHeight = image.height + + let bitmapBytesForRow = Int(imageWidth * 4) + let bitmapByteCount = bitmapBytesForRow * Int(imageHeight) + + if self.bitmapData == nil { + let imageRect = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let bitmapData = malloc(bitmapByteCount) + let bitmapInformation = CGImageAlphaInfo.premultipliedFirst.rawValue + + let colorContext = CGContext( + data: bitmapData, + width: imageWidth, + height: imageHeight, + bitsPerComponent: 8, + bytesPerRow: bitmapBytesForRow, + space: colorSpace, + bitmapInfo: bitmapInformation + ) + + colorContext?.clear(imageRect) + colorContext?.draw(image, in: imageRect) + + self.bitmapData = bitmapData + } + + self.bitmapData?.withMemoryRebound(to: UInt8.self, capacity: bitmapByteCount) { pointer in + let offset = 4 * ((Int(imageWidth) * Int(point.y)) + Int(point.x)) + + redComponent = CGFloat(pointer[offset + 1]) / 255.0 + greenComponent = CGFloat(pointer[offset + 2]) / 255.0 + blueComponent = CGFloat(pointer[offset + 3]) / 255.0 + } + + if let redComponent = redComponent, let greenComponent = greenComponent, let blueComponent = blueComponent { + return DrawingColor(rgb: UIColor(red: redComponent, green: greenComponent, blue: blueComponent, alpha: 1.0).rgb).withUpdatedPosition(position) + } else { + return nil + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let bitmapData = self.bitmapData { + free(bitmapData) + } + } + + @objc func handlePan(point: CGPoint) { + guard let size = self.validSize else { + return + } + var location = self.convert(point, from: nil) + location.x = max(0.0, min(size.width - 1.0, location.x)) + location.y = max(0.0, min(size.height - 1.0, location.y)) + if let color = self.getColor(at: location), color != self.selectedColor { + self.selected(color) + let _ = self.updateLayout(size: size, selectedColor: color) + } + } + + private var animatingIn = false + private var scheduledAnimateOut: (() -> Void)? + + func animateIn() { + self.animatingIn = true + + Queue.mainQueue().after(0.15) { + self.selected(DrawingColor(rgb: 0xffffff)) + } + + self.wrapper.mask = self.circleMaskView + self.circleMaskView.frame = self.bounds + + self.maskCircle.fillColor = UIColor.red.cgColor + self.circleMaskView.layer.addSublayer(self.maskCircle) + + self.maskCircle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0))).cgPath + self.maskCircle.frame = CGRect(origin: .zero, size: CGSize(width: 300.0, height: 300.0)) + self.maskCircle.position = CGPoint(x: 15.0, y: self.bounds.height - 15.0) + + self.maskCircle.transform = CATransform3DMakeScale(3.0, 3.0, 1.0) + self.maskCircle.animateScale(from: 0.05, to: 3.0, duration: 0.35, completion: { _ in + self.animatingIn = false + self.wrapper.mask = nil + + if let scheduledAnimateOut = self.scheduledAnimateOut { + self.scheduledAnimateOut = nil + self.animateOut(completion: scheduledAnimateOut) + } + }) + } + + func animateOut(completion: @escaping () -> Void) { + guard !self.animatingIn else { + self.scheduledAnimateOut = completion + return + } + + if let selectedColor = self.selectedColor { + self.selected(selectedColor) + } + + self.knob.opacity = 0.0 + self.knob.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.circle.opacity = 0.0 + self.circle.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + let filler = UIView(frame: self.bounds) + filler.backgroundColor = self.selectedColor?.toUIColor() ?? .white + self.wrapper.addSubview(filler) + + filler.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + self.wrapper.mask = self.circleMaskView + self.maskCircle.animatePosition(from: self.maskCircle.position, to: CGPoint(x: 16.0, y: self.bounds.height - 16.0), duration: 0.25, removeOnCompletion: false) + self.maskCircle.animateScale(from: 3.0, to: 0.06333, duration: 0.35, removeOnCompletion: false, completion: { _ in + + completion() + }) + } + + func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + let previousSize = self.validSize + + let imageSize = size + self.validSize = imageSize + + self.selectedColor = selectedColor + + if previousSize != imageSize { + if previousSize == nil { + self.layer.allowsGroupOpacity = true + self.isUserInteractionEnabled = true + self.wrapper.clipsToBounds = true + self.wrapper.layer.cornerRadius = 17.0 + } + + self.wrapper.frame = CGRect(origin: .zero, size: imageSize) + if self.wrapper.superview == nil { + self.addSubview(self.wrapper) + } + + if let bitmapData = self.bitmapData { + free(bitmapData) + } + self.image.image = generateSpectrumImage(size: imageSize) + self.image.frame = CGRect(origin: .zero, size: imageSize) + if self.image.superview == nil { + self.wrapper.addSubview(self.image) + } + } + + if let color = selectedColor, let position = color.position { + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.layer.addSublayer(self.knob) + + self.knob.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + if self.circle.superlayer == nil { + self.circle.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 26.0, height: 26.0))).cgPath + self.layer.addSublayer(self.circle) + + self.circle.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.knob.isHidden = false + self.circle.isHidden = false + + let margin: CGFloat = 10.0 + let knobSize = CGSize(width: 32.0, height: 32.0) + let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width * position.x - knobSize.width / 2.0), y: floorToScreenPixels(size.height * position.y - knobSize.height / 2.0) - 33.0), size: knobSize) + self.knob.frame = knobFrame.insetBy(dx: -margin, dy: -margin) + + self.circle.fillColor = color.toUIColor().cgColor + self.circle.frame = knobFrame.insetBy(dx: 3.0, dy: 3.0) + } else { + self.knob.isHidden = true + self.circle.isHidden = true + } + + return size + } +} + +private final class ColorSlidersComponent: CombinedComponent { + typealias EnvironmentType = ComponentFlow.Empty + + let color: DrawingColor + let updated: (DrawingColor) -> Void + + init( + color: DrawingColor, + updated: @escaping (DrawingColor) -> Void + ) { + self.color = color + self.updated = updated + } + + static func ==(lhs: ColorSlidersComponent, rhs: ColorSlidersComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + static var body: Body { + let redTitle = Child(MultilineTextComponent.self) + let redSlider = Child(ColorSliderComponent.self) + let redField = Child(ColorFieldComponent.self) + + let greenTitle = Child(MultilineTextComponent.self) + let greenSlider = Child(ColorSliderComponent.self) + let greenField = Child(ColorFieldComponent.self) + + let blueTitle = Child(MultilineTextComponent.self) + let blueSlider = Child(ColorSliderComponent.self) + let blueField = Child(ColorFieldComponent.self) + + let hexTitle = Child(MultilineTextComponent.self) + let hexField = Child(ColorFieldComponent.self) + + return { context in + let component = context.component + + var contentHeight: CGFloat = 0.0 + + let redTitle = redTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "RED", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(redTitle + .position(CGPoint(x: 5.0 + redTitle.size.width / 2.0, y: contentHeight + redTitle.size.height / 2.0)) + ) + contentHeight += redTitle.size.height + contentHeight += 8.0 + + let currentColor = component.color + let updateColor = component.updated + + let redSlider = redSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedRed(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedRed(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.red, + updated: { value in + updateColor(currentColor.withUpdatedRed(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(redSlider + .position(CGPoint(x: redSlider.size.width / 2.0, y: contentHeight + redSlider.size.height / 2.0)) + ) + + let redField = redField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.red * 255.0))", + updated: { value in + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedRed(CGFloat(intValue) / 255.0)) + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(redField + .position(CGPoint(x: context.availableSize.width - redField.size.width / 2.0, y: contentHeight + redField.size.height / 2.0)) + ) + + contentHeight += redSlider.size.height + contentHeight += 28.0 + + let greenTitle = greenTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "GREEN", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(greenTitle + .position(CGPoint(x: 5.0 + greenTitle.size.width / 2.0, y: contentHeight + greenTitle.size.height / 2.0)) + ) + contentHeight += greenTitle.size.height + contentHeight += 8.0 + + let greenSlider = greenSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedGreen(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedGreen(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.green, + updated: { value in + updateColor(currentColor.withUpdatedGreen(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(greenSlider + .position(CGPoint(x: greenSlider.size.width / 2.0, y: contentHeight + greenSlider.size.height / 2.0)) + ) + + let greenField = greenField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.green * 255.0))", + updated: { value in + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedGreen(CGFloat(intValue) / 255.0)) + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(greenField + .position(CGPoint(x: context.availableSize.width - greenField.size.width / 2.0, y: contentHeight + greenField.size.height / 2.0)) + ) + + contentHeight += greenSlider.size.height + contentHeight += 28.0 + + let blueTitle = blueTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "BLUE", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(blueTitle + .position(CGPoint(x: 5.0 + blueTitle.size.width / 2.0, y: contentHeight + blueTitle.size.height / 2.0)) + ) + contentHeight += blueTitle.size.height + contentHeight += 8.0 + + let blueSlider = blueSlider.update( + component: ColorSliderComponent( + leftColor: component.color.withUpdatedBlue(0.0).withUpdatedAlpha(1.0), + rightColor: component.color.withUpdatedBlue(1.0).withUpdatedAlpha(1.0), + currentColor: component.color, + value: component.color.blue, + updated: { value in + updateColor(currentColor.withUpdatedBlue(value)) + } + ), + availableSize: CGSize(width: context.availableSize.width - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(blueSlider + .position(CGPoint(x: blueSlider.size.width / 2.0, y: contentHeight + blueSlider.size.height / 2.0)) + ) + + let blueField = blueField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(component.color.blue * 255.0))", + updated: { value in + let intValue = Int(value) ?? 0 + updateColor(currentColor.withUpdatedBlue(CGFloat(intValue) / 255.0)) + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 255 { + return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(blueField + .position(CGPoint(x: context.availableSize.width - blueField.size.width / 2.0, y: contentHeight + blueField.size.height / 2.0)) + ) + + contentHeight += blueSlider.size.height + contentHeight += 28.0 + + let hexField = hexField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .text, + value: component.color.toUIColor().hexString.uppercased(), + updated: { value in + if value.count == 6, let uiColor = UIColor(hexString: value) { + updateColor(DrawingColor(color: uiColor).withUpdatedAlpha(currentColor.alpha)) + } + }, + shouldUpdate: { value in + if value.count <= 6 && value.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(hexField + .position(CGPoint(x: context.availableSize.width - hexField.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) + ) + + let hexTitle = hexTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Hex Color #", + font: Font.regular(17.0), + textColor: UIColor(rgb: 0xffffff), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(hexTitle + .position(CGPoint(x: context.availableSize.width - hexField.size.width - 12.0 - hexTitle.size.width / 2.0, y: contentHeight + hexField.size.height / 2.0)) + ) + + contentHeight += hexField.size.height + contentHeight += 8.0 + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private class SegmentedControlComponent: Component { + let values: [String] + let selectedIndex: Int + let selectionChanged: (Int) -> Void + + init(values: [String], selectedIndex: Int, selectionChanged: @escaping (Int) -> Void) { + self.values = values + self.selectedIndex = selectedIndex + self.selectionChanged = selectionChanged + } + + static func ==(lhs: SegmentedControlComponent, rhs: SegmentedControlComponent) -> Bool { + if lhs.values != rhs.values { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + return true + } + + final class View: UIView { + private let backgroundNode: NavigationBackgroundNode + private let node: SegmentedControlNode + + init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.1)) + self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0) + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundNode.view) + self.addSubview(self.node.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: SegmentedControlComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.node.items = component.values.map { SegmentedControlItem(title: $0) } + self.node.selectedIndex = component.selectedIndex + let selectionChanged = component.selectionChanged + self.node.selectedIndexChanged = { [weak self] index in + self?.window?.endEditing(true) + selectionChanged(index) + } + + let size = self.node.updateLayout(.stretchToFill(width: availableSize.width), transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: size)) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate) + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class ColorSwatchComponent: Component { + enum SwatchType: Equatable { + case main + case pallete(Bool) + } + + let type: SwatchType + let color: DrawingColor? + let tag: AnyObject? + let action: () -> Void + let holdAction: (() -> Void)? + let pan: ((CGPoint) -> Void)? + let release: (() -> Void)? + + init( + type: SwatchType, + color: DrawingColor?, + tag: AnyObject? = nil, + action: @escaping () -> Void, + holdAction: (() -> Void)? = nil, + pan: ((CGPoint) -> Void)? = nil, + release: (() -> Void)? = nil + ) { + self.type = type + self.color = color + self.tag = tag + self.action = action + self.holdAction = holdAction + self.pan = pan + self.release = release + } + + static func == (lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool { + return lhs.type == rhs.type && lhs.color == rhs.color + } + + final class View: UIButton, ComponentTaggedView { + private var component: ColorSwatchComponent? + + private var contentView: UIView + + private var ringLayer: CALayer? + private var ringMaskLayer: SimpleShapeLayer? + + private let circleLayer: SimpleShapeLayer + + private let fastCircleLayer: SimpleShapeLayer + + private var currentIsHighlighted: Bool = false { + didSet { + if self.currentIsHighlighted != oldValue { + self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0 + } + } + } + + private var holdActionTriggerred: Bool = false + private var holdActionTimer: Foundation.Timer? + + override init(frame: CGRect) { + self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size)) + self.contentView.isUserInteractionEnabled = false + self.circleLayer = SimpleShapeLayer() + self.fastCircleLayer = SimpleShapeLayer() + self.fastCircleLayer.fillColor = UIColor.white.cgColor + self.fastCircleLayer.isHidden = true + + super.init(frame: frame) + + self.addSubview(self.contentView) + self.contentView.layer.addSublayer(self.circleLayer) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + @objc private func pressed() { + if self.holdActionTriggerred { + self.holdActionTriggerred = false + } else { + self.component?.action() + } + } + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + self.currentIsHighlighted = true + + self.holdActionTriggerred = false + + if self.component?.holdAction != nil { + Queue.mainQueue().after(0.15, { + if self.currentIsHighlighted { + self.fastCircleLayer.isHidden = false + self.fastCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.fastCircleLayer.animateScale(from: 0.57575, to: 1.0, duration: 0.25) + } + }) + + self.holdActionTimer?.invalidate() + if #available(iOS 10.0, *) { + let holdActionTimer = Timer(timeInterval: 0.4, repeats: false, block: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.holdActionTriggerred = true + strongSelf.holdActionTimer?.invalidate() + strongSelf.component?.holdAction?() + Queue.mainQueue().after(0.1, { + strongSelf.fastCircleLayer.isHidden = true + }) + }) + self.holdActionTimer = holdActionTimer + RunLoop.main.add(holdActionTimer, forMode: .common) + } + } + + return super.beginTracking(touch, with: event) + } + + override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + if self.holdActionTriggerred { + let location = touch.location(in: nil) + self.component?.pan?(location) + } + return true + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + if self.holdActionTriggerred { + self.component?.release?() + } + + self.currentIsHighlighted = false + Queue.mainQueue().after(0.1) { + self.holdActionTriggerred = false + } + if !self.fastCircleLayer.isHidden { + let currentAlpha: CGFloat = CGFloat(self.fastCircleLayer.presentation()?.opacity ?? 1.0) + self.fastCircleLayer.animateAlpha(from: currentAlpha, to: 0.0, duration: 0.1, completion: { _ in + self.fastCircleLayer.isHidden = true + }) + } + + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + + super.endTracking(touch, with: event) + } + + override public func cancelTracking(with event: UIEvent?) { + if self.holdActionTriggerred { + self.component?.release?() + } + + self.currentIsHighlighted = false + self.holdActionTriggerred = false + + self.holdActionTimer?.invalidate() + self.holdActionTimer = nil + + super.cancelTracking(with: event) + } + + func animateIn() { + self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.contentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + + func animateOut() { + self.contentView.alpha = 0.0 + self.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + self.contentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + + func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + let contentSize: CGSize + if case .pallete = component.type { + contentSize = availableSize + } else { + contentSize = CGSize(width: 24.0, height: 24.0) + } + self.contentView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) / 2.0), y: floor((availableSize.height - contentSize.height) / 2.0)), size: contentSize) + + let bounds = CGRect(origin: .zero, size: contentSize) + switch component.type { + case .main: + self.circleLayer.frame = bounds + if self.circleLayer.path == nil { + self.circleLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3.0, dy: 3.0)).cgPath + } + + let ringFrame = bounds.insetBy(dx: -1.0, dy: -1.0) + if self.ringLayer == nil { + let ringLayer = SimpleLayer() + ringLayer.contents = UIImage(bundleImageName: "Media Editor/RoundSpectrum")?.cgImage + ringLayer.frame = ringFrame + self.contentView.layer.addSublayer(ringLayer) + + self.ringLayer = ringLayer + + let ringMaskLayer = SimpleShapeLayer() + ringMaskLayer.frame = CGRect(origin: .zero, size: ringFrame.size) + ringMaskLayer.strokeColor = UIColor.white.cgColor + ringMaskLayer.fillColor = UIColor.clear.cgColor + self.ringMaskLayer = ringMaskLayer + self.ringLayer?.mask = ringMaskLayer + } + + if let ringMaskLayer = self.ringMaskLayer { + if component.color == nil { + transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 7.0, dy: 7.0)).cgPath) + transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 12.0) + } else { + transition.setShapeLayerPath(layer: ringMaskLayer, path: UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 1.0, dy: 1.0)).cgPath) + transition.setShapeLayerLineWidth(layer: ringMaskLayer, lineWidth: 2.0) + } + } + + if self.fastCircleLayer.path == nil { + self.fastCircleLayer.path = UIBezierPath(ovalIn: bounds).cgPath + self.fastCircleLayer.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - bounds.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - bounds.size.height) / 2.0)), size: bounds.size) + self.layer.addSublayer(self.fastCircleLayer) + } + case let .pallete(selected): + self.layer.allowsGroupOpacity = true + self.contentView.layer.allowsGroupOpacity = true + + self.circleLayer.frame = bounds + if self.ringLayer == nil { + let ringLayer = SimpleLayer() + ringLayer.backgroundColor = UIColor.clear.cgColor + ringLayer.cornerRadius = contentSize.width / 2.0 + ringLayer.borderWidth = 3.0 + ringLayer.frame = CGRect(origin: .zero, size: contentSize) + self.contentView.layer.insertSublayer(ringLayer, at: 0) + self.ringLayer = ringLayer + } + + if selected { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 5.0, dy: 5.0), transform: nil)) + } else { + transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil)) + } + } + + if let color = component.color { + self.circleLayer.fillColor = color.toCGColor() + if case .pallete = component.type { + if color.toUIColor().rgb == 0x000000 { + self.circleLayer.strokeColor = UIColor(rgb: 0x1f1f1f).cgColor + self.circleLayer.lineWidth = 1.0 + self.ringLayer?.borderColor = UIColor(rgb: 0x1f1f1f).cgColor + } else { + self.ringLayer?.borderColor = color.toCGColor() + } + } + } + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn() + case .animateOut: + self.animateOut() + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +class BlurredRectangle: Component { + let color: UIColor + let radius: CGFloat + + init(color: UIColor, radius: CGFloat = 0.0) { + self.color = color + self.radius = radius + } + + static func ==(lhs: BlurredRectangle, rhs: BlurredRectangle) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + if lhs.radius != rhs.radius { + return false + } + return true + } + + final class View: UIView { + private let background: NavigationBackgroundNode + + init() { + self.background = NavigationBackgroundNode(color: .clear) + + super.init(frame: CGRect()) + + self.addSubview(self.background.view) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: BlurredRectangle, availableSize: CGSize, transition: Transition) -> CGSize { + transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.background.updateColor(color: component.color, transition: .immediate) + self.background.update(size: availableSize, cornerRadius: component.radius, transition: .immediate) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +private final class ColorPickerContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialColor: DrawingColor + let colorChanged: (DrawingColor) -> Void + let eyedropper: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + initialColor: DrawingColor, + colorChanged: @escaping (DrawingColor) -> Void, + eyedropper: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.initialColor = initialColor + self.colorChanged = colorChanged + self.eyedropper = eyedropper + self.dismiss = dismiss + } + + static func ==(lhs: ColorPickerContent, rhs: ColorPickerContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var cachedEyedropperImage: UIImage? + var eyedropperImage: UIImage { + let eyedropperImage: UIImage + if let image = self.cachedEyedropperImage { + eyedropperImage = image + } else { + eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)! + self.cachedEyedropperImage = eyedropperImage + } + return eyedropperImage + } + + var cachedCloseImage: UIImage? + var closeImage: UIImage { + let closeImage: UIImage + if let image = self.cachedCloseImage { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))! + self.cachedCloseImage = closeImage + } + return closeImage + } + + var selectedMode: Int = 0 + var selectedColor: DrawingColor + + var savedColors: [DrawingColor] = [] + + var colorChanged: (DrawingColor) -> Void = { _ in } + + init(initialColor: DrawingColor) { + self.selectedColor = initialColor + + self.savedColors = [DrawingColor(color: .red), DrawingColor(color: .green), DrawingColor(color: .blue)] + } + + func updateColor(_ color: DrawingColor, keepAlpha: Bool = false) { + self.selectedColor = keepAlpha ? color.withUpdatedAlpha(self.selectedColor.alpha) : color + self.colorChanged(self.selectedColor) + self.updated(transition: .immediate) + } + + func updateAlpha(_ alpha: CGFloat) { + self.selectedColor = self.selectedColor.withUpdatedAlpha(alpha) + self.colorChanged(self.selectedColor) + self.updated(transition: .immediate) + } + + func updateSelectedMode(_ mode: Int) { + self.selectedMode = mode + self.updated(transition: .easeInOut(duration: 0.2)) + } + + func saveCurrentColor() { + self.savedColors.append(self.selectedColor) + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func makeState() -> State { + return State(initialColor: self.initialColor) + } + + static var body: Body { + let eyedropperButton = Child(Button.self) + let closeButton = Child(Button.self) + let title = Child(MultilineTextComponent.self) + let modeControl = Child(SegmentedControlComponent.self) + + let colorGrid = Child(ColorGridComponent.self) + let colorSpectrum = Child(ColorSpectrumComponent.self) + let colorSliders = Child(ColorSlidersComponent.self) + + let opacityTitle = Child(MultilineTextComponent.self) + let opacitySlider = Child(ColorSliderComponent.self) + let opacityField = Child(ColorFieldComponent.self) + + let divider = Child(Rectangle.self) + + let preview = Child(ColorPreviewComponent.self) + + let swatch1Button = Child(ColorSwatchComponent.self) + let swatch2Button = Child(ColorSwatchComponent.self) + let swatch3Button = Child(ColorSwatchComponent.self) + let swatch4Button = Child(ColorSwatchComponent.self) + let swatch5Button = Child(ColorSwatchComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let state = context.state + state.colorChanged = component.colorChanged + + let sideInset: CGFloat = 16.0 + + let eyedropperButton = eyedropperButton.update( + component: Button( + content: AnyComponent( + Image(image: state.eyedropperImage) + ), + action: { [weak component] in + component?.eyedropper() + } + ).minSize(CGSize(width: 30.0, height: 30.0)), + availableSize: CGSize(width: 19.0, height: 19.0), + transition: .immediate + ) + context.add(eyedropperButton + .position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0)) + ) + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + BlurredRectangle( + color: UIColor(rgb: 0x888888, alpha: 0.1), + radius: 15.0 + ) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: state.closeImage) + ) + ), + ])), + action: { [weak component] in + component?.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0)) + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Colors", + font: Font.semibold(17.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - 100.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0)) + ) + + var contentHeight: CGFloat = 58.0 + + let modeControl = modeControl.update( + component: SegmentedControlComponent( + values: ["Grid", "Spectrum", "Sliders"], + selectedIndex: 0, + selectionChanged: { [weak state] index in + state?.updateSelectedMode(index) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(modeControl + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + modeControl.size.height / 2.0)) + ) + contentHeight += modeControl.size.height + contentHeight += 20.0 + + let squareSize = floorToScreenPixels((context.availableSize.width - sideInset * 2.0) / 12.0) + let fieldSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: squareSize * 10.0) + + if state.selectedMode == 0 { + let colorGrid = colorGrid.update( + component: ColorGridComponent( + color: state.selectedColor, + selected: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(colorGrid + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorGrid.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } else if state.selectedMode == 1 { + let colorSpectrum = colorSpectrum.update( + component: ColorSpectrumComponent( + color: state.selectedColor, + selected: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: fieldSize, + transition: .immediate + ) + context.add(colorSpectrum + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + fieldSize.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } else if state.selectedMode == 2 { + let colorSliders = colorSliders.update( + component: ColorSlidersComponent( + color: state.selectedColor, + updated: { [weak state] color in + state?.updateColor(color, keepAlpha: true) + } + ), + availableSize: fieldSize, + transition: .immediate + ) + context.add(colorSliders + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + colorSliders.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default()) + ) + } + + contentHeight += fieldSize.height + contentHeight += 21.0 + + let opacityTitle = opacityTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: "OPACITY", + font: Font.semibold(13.0), + textColor: UIColor(rgb: 0x9b9da5), + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(opacityTitle + .position(CGPoint(x: sideInset + 5.0 + opacityTitle.size.width / 2.0, y: contentHeight + opacityTitle.size.height / 2.0)) + ) + contentHeight += opacityTitle.size.height + contentHeight += 8.0 + + let opacitySlider = opacitySlider.update( + component: ColorSliderComponent( + leftColor: state.selectedColor.withUpdatedAlpha(0.0), + rightColor: state.selectedColor.withUpdatedAlpha(1.0), + currentColor: state.selectedColor, + value: state.selectedColor.alpha, + updated: { value in + state.updateAlpha(value) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 89.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(opacitySlider + .position(CGPoint(x: sideInset + opacitySlider.size.width / 2.0, y: contentHeight + opacitySlider.size.height / 2.0)) + ) + + let opacityField = opacityField.update( + component: ColorFieldComponent( + backgroundColor: UIColor(rgb: 0x000000, alpha: 0.6), + textColor: .white, + type: .number, + value: "\(Int(state.selectedColor.alpha * 100.0))", + suffix: "%", + updated: { value in + let intValue = Int(value) ?? 0 + state.updateAlpha(CGFloat(intValue) / 100.0) + }, + shouldUpdate: { value in + if let intValue = Int(value), intValue >= 0 && intValue <= 100 { + return true + } else if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } else { + return false + } + } + ), + availableSize: CGSize(width: 77.0, height: 36.0), + transition: .immediate + ) + context.add(opacityField + .position(CGPoint(x: context.availableSize.width - sideInset - opacityField.size.width / 2.0, y: contentHeight + opacityField.size.height / 2.0)) + ) + + contentHeight += opacitySlider.size.height + contentHeight += 24.0 + + let divider = divider.update( + component: Rectangle(color: UIColor(rgb: 0x48484a)), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1.0), + transition: .immediate + ) + context.add(divider + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) + ) + contentHeight += divider.size.height + contentHeight += 22.0 + + let preview = preview.update( + component: ColorPreviewComponent( + color: state.selectedColor + ), + availableSize: CGSize(width: 82.0, height: 82.0), + transition: .immediate + ) + context.add(preview + .position(CGPoint(x: sideInset + preview.size.width / 2.0, y: contentHeight + preview.size.height / 2.0)) + ) + + + var swatchOffset: CGFloat = sideInset + preview.size.width + 38.0 + let swatchSpacing: CGFloat = 20.0 + + let swatch1Button = swatch1Button.update( + component: ColorSwatchComponent( + type: .pallete(state.selectedColor == DrawingColor(color: .black)), + color: DrawingColor(color: .black), + action: { + state.updateColor(DrawingColor(color: .black)) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch1Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch1Button.size.height / 2.0)) + ) + swatchOffset += swatch1Button.size.width + swatchSpacing + + let swatch2Button = swatch2Button.update( + component: ColorSwatchComponent( + type: .pallete(state.selectedColor == DrawingColor(rgb: 0x0161fd)), + color: DrawingColor(rgb: 0x0161fd), + action: { + state.updateColor(DrawingColor(rgb: 0x0161fd)) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch2Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch2Button.size.height / 2.0)) + ) + swatchOffset += swatch2Button.size.width + swatchSpacing + + let swatch3Button = swatch3Button.update( + component: ColorSwatchComponent( + type: .pallete(state.selectedColor == DrawingColor(rgb: 0x32c759)), + color: DrawingColor(rgb: 0x32c759), + action: { + state.updateColor(DrawingColor(rgb: 0x32c759)) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch3Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch3Button.size.height / 2.0)) + ) + swatchOffset += swatch3Button.size.width + swatchSpacing + + let swatch4Button = swatch4Button.update( + component: ColorSwatchComponent( + type: .pallete(state.selectedColor == DrawingColor(rgb: 0xffcc02)), + color: DrawingColor(rgb: 0xffcc02), + action: { + state.updateColor(DrawingColor(rgb: 0xffcc02)) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch4Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch4Button.size.height / 2.0)) + ) + swatchOffset += swatch4Button.size.width + swatchSpacing + + let swatch5Button = swatch5Button.update( + component: ColorSwatchComponent( + type: .pallete(state.selectedColor == DrawingColor(rgb: 0xff3a30)), + color: DrawingColor(rgb: 0xff3a30), + action: { + state.updateColor(DrawingColor(rgb: 0xff3a30)) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: context.transition + ) + context.add(swatch5Button + .position(CGPoint(x: swatchOffset, y: contentHeight + swatch5Button.size.height / 2.0)) + ) + + contentHeight += preview.size.height + contentHeight += 10.0 + + let bottomPanelPadding: CGFloat = 12.0 + var bottomInset: CGFloat + if case .regular = environment.metrics.widthClass { + bottomInset = bottomPanelPadding + } else { + bottomInset = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + } + + if environment.inputHeight > 0.0 { + bottomInset += environment.inputHeight - bottomInset - 120.0 + } + + return CGSize(width: context.availableSize.width, height: contentHeight + bottomInset) + } + } +} + +private final class ColorPickerSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let initialColor: DrawingColor + private let updated: (DrawingColor) -> Void + private let openEyedropper: () -> Void + private let dismissed: () -> Void + + init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void) { + self.context = context + self.initialColor = initialColor + self.updated = updated + self.openEyedropper = openEyedropper + self.dismissed = dismissed + } + + static func ==(lhs: ColorPickerSheetComponent, rhs: ColorPickerSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let updated = context.component.updated + let openEyedropper = context.component.openEyedropper + let dismissed = context.component.dismissed + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(ColorPickerContent( + context: context.component.context, + initialColor: context.component.initialColor, + colorChanged: { color in + updated(color) + }, + eyedropper: { + openEyedropper() + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + }, + dismiss: { + dismissed() + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.dark), + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +class ColorPickerScreen: ViewControllerComponentContainer { + init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { + super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.navigationPresentation = .flatModal + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/DrawingUI/Sources/ConcaveHull.swift b/submodules/DrawingUI/Sources/ConcaveHull.swift new file mode 100644 index 00000000000..3fb50fd27f4 --- /dev/null +++ b/submodules/DrawingUI/Sources/ConcaveHull.swift @@ -0,0 +1,308 @@ +import Foundation + +private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool { + func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool { + let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x)) + return ccw > 0 ? true : ccw < 0 ? false : true + } + let segment1 = seg1[0] + let segment2 = seg1[1] + let segment3 = seg2[0] + let segment4 = seg2[1] + return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4) + && ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4) +} + +private func convex(points: [CGPoint]) -> [CGPoint] { + func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double { + return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) + } + + func upperTangent(_ points: [CGPoint]) -> [CGPoint] { + var lower: [CGPoint] = [] + for point in points { + while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) { + _ = lower.popLast() + } + lower.append(point) + } + _ = lower.popLast() + return lower + } + + func lowerTangent(_ points: [CGPoint]) -> [CGPoint] { + let reversed = points.reversed() + var upper: [CGPoint] = [] + for point in reversed { + while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) { + _ = upper.popLast() + } + upper.append(point) + } + _ = upper.popLast() + return upper + } + + var convex: [CGPoint] = [] + convex.append(contentsOf: upperTangent(points)) + convex.append(contentsOf: lowerTangent(points)) + return convex +} + +private class Grid { + var cells = [Int: [Int: [CGPoint]]]() + var cellSize: Double = 0 + + init(_ points: [CGPoint], _ cellSize: Double) { + self.cellSize = cellSize + for point in points { + let cellXY = point2CellXY(point) + let x = cellXY[0] + let y = cellXY[1] + if self.cells[x] == nil { + self.cells[x] = [Int: [CGPoint]]() + } + if self.cells[x]![y] == nil { + self.cells[x]![y] = [CGPoint]() + } + self.cells[x]![y]!.append(point) + } + } + + func point2CellXY(_ point: CGPoint) -> [Int] { + let x = Int(point.x / self.cellSize) + let y = Int(point.y / self.cellSize) + return [x, y] + } + + func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] { + return [ + bbox[0] - (scaleFactor * self.cellSize), + bbox[1] - (scaleFactor * self.cellSize), + bbox[2] + (scaleFactor * self.cellSize), + bbox[3] + (scaleFactor * self.cellSize) + ] + } + + func removePoint(_ point: CGPoint) { + let cellXY = point2CellXY(point) + let cell = self.cells[cellXY[0]]![cellXY[1]]! + var pointIdxInCell = 0 + for idx in 0 ..< cell.count { + if cell[idx].x == point.x && cell[idx].y == point.y { + pointIdxInCell = idx + break + } + } + self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell) + } + + func rangePoints(_ bbox: [Double]) -> [CGPoint] { + let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1])) + let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3])) + var points: [CGPoint] = [] + for x in tlCellXY[0].. [CGPoint] { + if let x = self.cells[xAbs] { + if let y = x[yOrd] { + return y + } + } + return [] + } + +} + +private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi)) + +private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] { + let sortedSet = sortByX(pointSet) + return sortedSet.filter { (point: CGPoint) -> Bool in + let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in + return idx.x == point.x && idx.y == point.y + }) + if index == 0 { + return true + } else { + let prevEl = pointSet[index! - 1] + if prevEl.x != point.x || prevEl.y != point.y { + return true + } + return false + } + } +} + +private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] { + return pointSet.sorted(by: { (lhs, rhs) -> Bool in + if lhs.x == rhs.x { + return lhs.y < rhs.y + } else { + return lhs.x < rhs.x + } + }) +} + +private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double { + return pow(b.x - a.x, 2) + pow(b.y - a.y, 2) +} + +private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double { + let aShifted = [a.x - o.x, a.y - o.y] + let bShifted = [b.x - o.x, b.y - o.y] + let sqALen = squaredLength(o, a) + let sqBLen = squaredLength(o, b) + let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1] + return dot / sqrt(sqALen * sqBLen) +} + +private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool { + for idx in 0.. CGPoint { + var minX = Double.infinity + var minY = Double.infinity + var maxX = -Double.infinity + var maxY = -Double.infinity + for idx in 0 ..< points.reversed().count { + if points[idx].x < minX { + minX = points[idx].x + } + if points[idx].y < minY { + minY = points[idx].y + } + if points[idx].x > maxX { + maxX = points[idx].x + } + if points[idx].y > maxY { + maxY = points[idx].y + } + } + return CGPoint(x: maxX - minX, y: maxY - minY) +} + +private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] { + return [min(edge[0].x, edge[1].x), + min(edge[0].y, edge[1].y), + max(edge[0].x, edge[1].x), + max(edge[0].y, edge[1].y)] +} + +private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? { + var point: CGPoint? + var angle1Cos = maxConcaveAngleCos + var angle2Cos = maxConcaveAngleCos + var a1Cos: Double = 0 + var a2Cos: Double = 0 + for innerPoint in innerPoints { + a1Cos = cosFunc(edge[0], edge[1], innerPoint) + a2Cos = cosFunc(edge[1], edge[0], innerPoint) + if a1Cos > angle1Cos && + a2Cos > angle2Cos && + !intersectFunc([edge[0], innerPoint], convex) && + !intersectFunc([edge[1], innerPoint], convex) { + angle1Cos = a1Cos + angle2Cos = a2Cos + point = innerPoint + } + } + return point +} + +private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] { + var edge: [CGPoint] + var keyInSkipList: String = "" + var scaleFactor: Double + var midPoint: CGPoint? + var bBoxAround: [Double] + var bBoxWidth: Double = 0 + var bBoxHeight: Double = 0 + var midPointInserted: Bool = false + + for idx in 0.. bBoxWidth || maxSearchArea[1] > bBoxHeight) + + if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] { + edgeSkipList[keyInSkipList] = true + } + if let midPoint = midPoint { + convex.insert(midPoint, at: idx + 1) + grid.removePoint(midPoint) + midPointInserted = true + } + } + + if midPointInserted { + return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList) + } + + return convex +} + +private extension CGPoint { + var key: String { + return "\(self.x),\(self.y)" + } +} + +func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] { + let points = filterDuplicates(points) + let occupiedArea = occupiedAreaFunc(points) + let maxSearchArea: [Double] = [ + occupiedArea.x * 0.6, + occupiedArea.y * 0.6 + ] + + var convex = convex(points: points) + + var innerPoints = points.filter { (point: CGPoint) -> Bool in + let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in + return idx.x == point.x && idx.y == point.y + }) + return idx == nil + } + + innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in + return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x + }) + + let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count)) + let grid = Grid(innerPoints, cellSize) + + var skipList: [String: Bool] = [String: Bool]() + return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList) +} diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift new file mode 100644 index 00000000000..a182caee136 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -0,0 +1,511 @@ +import Foundation +import UIKit +import Display +import AccountContext + +public final class DrawingBubbleEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case drawType + case color + case lineWidth + case referenceDrawingSize + case position + case size + case rotation + case tailPosition + case renderImage + } + + enum DrawType: Codable { + case fill + case stroke + } + + public let uuid: UUID + public let isAnimated: Bool + + var drawType: DrawType + public var color: DrawingColor + public var lineWidth: CGFloat + + var referenceDrawingSize: CGSize + public var position: CGPoint + public var size: CGSize + public var rotation: CGFloat + var tailPosition: CGPoint + + public var center: CGPoint { + return self.position + } + + public var scale: CGFloat = 1.0 + + public var renderImage: UIImage? + + init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.drawType = drawType + self.color = color + self.lineWidth = lineWidth + + self.referenceDrawingSize = .zero + self.position = .zero + self.size = CGSize(width: 1.0, height: 1.0) + self.rotation = 0.0 + self.tailPosition = CGPoint(x: 0.16, y: 0.18) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = false + self.drawType = try container.decode(DrawType.self, forKey: .drawType) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.size = try container.decode(CGSize.self, forKey: .size) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.drawType, forKey: .drawType) + try container.encode(self.color, forKey: .color) + try container.encode(self.lineWidth, forKey: .lineWidth) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.size, forKey: .size) + try container.encode(self.rotation, forKey: .rotation) + try container.encode(self.tailPosition, forKey: .tailPosition) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingBubbleEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + self.renderImage = (self.currentEntityView as? DrawingBubbleEntityView)?.getRenderImage() + } +} + +final class DrawingBubbleEntityView: DrawingEntityView { + private var bubbleEntity: DrawingBubbleEntity { + return self.entity as! DrawingBubbleEntity + } + + private var currentSize: CGSize? + private var currentTailPosition: CGPoint? + + private let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingBubbleEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(animated: Bool) { + let size = self.bubbleEntity.size + + self.center = self.bubbleEntity.position + self.bounds = CGRect(origin: .zero, size: size) + self.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation) + + if size != self.currentSize || self.bubbleEntity.tailPosition != self.currentTailPosition { + self.currentSize = size + self.currentTailPosition = self.bubbleEntity.tailPosition + self.shapeLayer.frame = self.bounds + + let cornerRadius = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.045) + let smallCornerRadius = max(5.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.01) + let tailWidth = max(5.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1) + + self.shapeLayer.path = CGPath.bubble(in: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius, smallCornerRadius: smallCornerRadius, tailPosition: self.bubbleEntity.tailPosition, tailWidth: tailWidth) + } + + switch self.bubbleEntity.drawType { + case .fill: + self.shapeLayer.fillColor = self.bubbleEntity.color.toCGColor() + self.shapeLayer.strokeColor = UIColor.clear.cgColor + case .stroke: + let minLineWidth = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.01) + let maxLineWidth = max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.05) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.bubbleEntity.lineWidth + + self.shapeLayer.fillColor = UIColor.clear.cgColor + self.shapeLayer.strokeColor = self.bubbleEntity.color.toCGColor() + self.shapeLayer.lineWidth = lineWidth + } + + super.update(animated: animated) + } + + fileprivate var visualLineWidth: CGFloat { + return self.shapeLayer.lineWidth + } + + private var maxLineWidth: CGFloat { + return max(10.0, max(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) * 0.1) + } + + fileprivate var minimumSize: CGSize { + let minSize = min(self.bubbleEntity.referenceDrawingSize.width, self.bubbleEntity.referenceDrawingSize.height) + return CGSize(width: minSize * 0.2, height: minSize * 0.2) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let lineWidth = self.maxLineWidth * 0.5 + let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth) + if expandedBounds.contains(point) { + return true + } + return false + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if case .stroke = self.bubbleEntity.drawType, var path = self.shapeLayer.path { + path = path.copy(strokingWithWidth: maxLineWidth * 0.8, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0) + if path.contains(point) { + return true + } else { + return false + } + } else { + return super.precisePoint(inside: point) + } + } + + override func updateSelectionView() { + super.updateSelectionView() + + guard let selectionView = self.selectionView as? DrawingBubbleEntititySelectionView else { + return + } + + selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation) + selectionView.setNeedsLayout() + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingBubbleEntititySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) + self.drawHierarchy(in: rect, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } +} + +final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let leftHandle = SimpleShapeLayer() + private let topLeftHandle = SimpleShapeLayer() + private let topHandle = SimpleShapeLayer() + private let topRightHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + private let bottomLeftHandle = SimpleShapeLayer() + private let bottomHandle = SimpleShapeLayer() + private let bottomRightHandle = SimpleShapeLayer() + private let tailHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle, + self.tailHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + for handle in handles { + handle.bounds = handleBounds + if handle === self.tailHandle { + handle.fillColor = UIColor(rgb: 0x00ff00).cgColor + } else { + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + } + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + + self.snapTool.onSnapXUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToXAxis(snapped) + } + } + + self.snapTool.onSnapYUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToYAxis(snapped) + } + } + + self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToAngle(snappedAngle) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 5.5 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView as? DrawingBubbleEntityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + return + } + } + } + self.currentHandle = self.layer + case .changed: + let delta = gestureRecognizer.translation(in: entityView.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedSize = entity.size + var updatedPosition = entity.position + var updatedTailPosition = entity.tailPosition + + let minimumSize = entityView.minimumSize + + if self.currentHandle === self.leftHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) + updatedPosition.x -= delta.x * -0.5 + } else if self.currentHandle === self.rightHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) + updatedPosition.x += delta.x * 0.5 + } else if self.currentHandle === self.topHandle { + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomHandle { + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topLeftHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) + updatedPosition.x -= delta.x * -0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topRightHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) + updatedPosition.x += delta.x * 0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomLeftHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) + updatedPosition.x -= delta.x * -0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomRightHandle { + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) + updatedPosition.x += delta.x * 0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.tailHandle { + updatedTailPosition = CGPoint(x: max(0.0, min(1.0, updatedTailPosition.x + delta.x / updatedSize.width)), y: max(0.0, min(updatedSize.height, updatedTailPosition.y + delta.y))) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + } + + entity.size = updatedSize + entity.position = updatedPosition + entity.tailPosition = updatedTailPosition + entityView.update(animated: false) + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + self.snapTool.reset() + case .cancelled: + self.snapTool.reset() + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + default: + break + } + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) || self.tailHandle.frame.contains(point) + } + + override func layoutSubviews() { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else { + return + } + + let inset = self.selectionInset + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle, + self.tailHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.topLeftHandle.position = CGPoint(x: inset, y: inset) + self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset) + self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset) + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset) + self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset) + self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset) + + let selectionScale = (self.bounds.width - inset * 2.0) / (max(0.001, entity.size.width)) + self.tailHandle.position = CGPoint(x: inset + (self.bounds.width - inset * 2.0) * entity.tailPosition.x, y: self.bounds.height - inset + entity.tailPosition.y * selectionScale) + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift new file mode 100644 index 00000000000..d13efe89b48 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -0,0 +1,781 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import AccountContext + + +public protocol DrawingEntity: AnyObject { + var uuid: UUID { get } + var isAnimated: Bool { get } + var center: CGPoint { get } + + var lineWidth: CGFloat { get set } + var color: DrawingColor { get set } + + var scale: CGFloat { get set } + + func duplicate() -> DrawingEntity + + var currentEntityView: DrawingEntityView? { get } + func makeView(context: AccountContext) -> DrawingEntityView + + func prepareForRender() +} + +enum CodableDrawingEntity { + case sticker(DrawingStickerEntity) + case text(DrawingTextEntity) + case simpleShape(DrawingSimpleShapeEntity) + case bubble(DrawingBubbleEntity) + case vector(DrawingVectorEntity) + + init?(entity: DrawingEntity) { + if let entity = entity as? DrawingStickerEntity { + self = .sticker(entity) + } else if let entity = entity as? DrawingTextEntity { + self = .text(entity) + } else if let entity = entity as? DrawingSimpleShapeEntity { + self = .simpleShape(entity) + } else if let entity = entity as? DrawingBubbleEntity { + self = .bubble(entity) + } else if let entity = entity as? DrawingVectorEntity { + self = .vector(entity) + } else { + return nil + } + } + + var entity: DrawingEntity { + switch self { + case let .sticker(entity): + return entity + case let .text(entity): + return entity + case let .simpleShape(entity): + return entity + case let .bubble(entity): + return entity + case let .vector(entity): + return entity + } + } +} + +public func decodeDrawingEntities(data: Data) -> [DrawingEntity] { + if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) { + return codableEntities.map { $0.entity } + } + return [] +} + +extension CodableDrawingEntity: Codable { + private enum CodingKeys: String, CodingKey { + case type + case entity + } + + private enum EntityType: Int, Codable { + case sticker + case text + case simpleShape + case bubble + case vector + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(EntityType.self, forKey: .type) + switch type { + case .sticker: + self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity)) + case .text: + self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity)) + case .simpleShape: + self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity)) + case .bubble: + self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity)) + case .vector: + self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity)) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .sticker(payload): + try container.encode(EntityType.sticker, forKey: .type) + try container.encode(payload, forKey: .entity) + case let .text(payload): + try container.encode(EntityType.text, forKey: .type) + try container.encode(payload, forKey: .entity) + case let .simpleShape(payload): + try container.encode(EntityType.simpleShape, forKey: .type) + try container.encode(payload, forKey: .entity) + case let .bubble(payload): + try container.encode(EntityType.bubble, forKey: .type) + try container.encode(payload, forKey: .entity) + case let .vector(payload): + try container.encode(EntityType.vector, forKey: .type) + try container.encode(payload, forKey: .entity) + } + } +} + +public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { + private let context: AccountContext + private let size: CGSize + + weak var drawingView: DrawingView? + weak var selectionContainerView: DrawingSelectionContainerView? + + private var tapGestureRecognizer: UITapGestureRecognizer! + private(set) var selectedEntityView: DrawingEntityView? + + public var getEntityCenterPosition: () -> CGPoint = { return .zero } + public var getEntityInitialRotation: () -> CGFloat = { return 0.0 } + public var hasSelectionChanged: (Bool) -> Void = { _ in } + var selectionChanged: (DrawingEntity?) -> Void = { _ in } + var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in } + + var entityAdded: (DrawingEntity) -> Void = { _ in } + var entityRemoved: (DrawingEntity) -> Void = { _ in } + + private let xAxisView = UIView() + private let yAxisView = UIView() + private let angleLayer = SimpleShapeLayer() + private let hapticFeedback = HapticFeedback() + + public init(context: AccountContext, size: CGSize) { + self.context = context + self.size = size + + super.init(frame: CGRect(origin: .zero, size: size)) + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + self.tapGestureRecognizer = tapGestureRecognizer + + self.xAxisView.alpha = 0.0 + self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.xAxisView.isUserInteractionEnabled = false + + self.yAxisView.alpha = 0.0 + self.yAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.yAxisView.isUserInteractionEnabled = false + + self.angleLayer.strokeColor = UIColor(rgb: 0xffd70a).cgColor + self.angleLayer.opacity = 0.0 + self.angleLayer.lineDashPattern = [12, 12] as [NSNumber] + + self.addSubview(self.xAxisView) + self.addSubview(self.yAxisView) + self.layer.addSublayer(self.angleLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + print() + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let point = self.getEntityCenterPosition() + self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 6.0, height: 3000.0)) + self.xAxisView.center = point + self.xAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation()) + + self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0)) + self.yAxisView.center = point + self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation()) + + let anglePath = CGMutablePath() + anglePath.move(to: CGPoint(x: 0.0, y: 3.0)) + anglePath.addLine(to: CGPoint(x: 3000.0, y: 3.0)) + self.angleLayer.path = anglePath + self.angleLayer.lineWidth = 6.0 + self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0)) + } + + var entities: [DrawingEntity] { + var entities: [DrawingEntity] = [] + for case let view as DrawingEntityView in self.subviews { + entities.append(view.entity) + } + return entities + } + + private var initialEntitiesData: Data? + public func setup(withEntitiesData entitiesData: Data?) { + self.clear() + + self.initialEntitiesData = entitiesData + + if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) { + let entities = codableEntities.map { $0.entity } + for entity in entities { + self.add(entity, announce: false) + } + } + } + + var entitiesData: Data? { + let entities = self.entities + guard !entities.isEmpty else { + return nil + } + for entity in entities { + entity.prepareForRender() + } + let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) }) + if let data = try? JSONEncoder().encode(codableEntities) { + return data + } else { + return nil + } + } + + var hasChanges: Bool { + if let initialEntitiesData = self.initialEntitiesData { + let entitiesData = self.entitiesData + return entitiesData != initialEntitiesData + } else { + return !self.entities.isEmpty + } + } + + private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint { + let offsetLength = round(self.size.width * 0.1) + let offset = CGPoint(x: offsetLength, y: offsetLength) + if let entity = entity { + return entity.center.offsetBy(dx: offset.x, dy: offset.y) + } else { + let minimalDistance: CGFloat = round(offsetLength * 0.5) + var position = self.getEntityCenterPosition() + + while true { + var occupied = false + for case let view as DrawingEntityView in self.subviews { + let location = view.entity.center + let distance = sqrt(pow(location.x - position.x, 2) + pow(location.y - position.y, 2)) + if distance < minimalDistance { + occupied = true + } + } + if !occupied { + break + } else { + position = position.offsetBy(dx: offset.x, dy: offset.y) + } + } + return position + } + } + + private func newEntitySize() -> CGSize { + let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) + let width = round(self.size.width * 0.5) * zoomScale + return CGSize(width: width, height: width) + } + + func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { + let center = self.startPosition(relativeTo: relativeTo) + let rotation = self.getEntityInitialRotation() + let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) + + if let shape = entity as? DrawingSimpleShapeEntity { + shape.position = center + shape.rotation = rotation + + if setup { + let size = self.newEntitySize() + shape.referenceDrawingSize = self.size + if shape.shapeType == .star { + shape.size = size + } else { + shape.size = CGSize(width: size.width, height: round(size.height * 0.75)) + } + } + } else if let vector = entity as? DrawingVectorEntity { + if setup { + vector.drawingSize = self.size + vector.referenceDrawingSize = self.size + vector.start = CGPoint(x: center.x * 0.5, y: center.y) + vector.mid = (0.5, 0.0) + vector.end = CGPoint(x: center.x * 1.5, y: center.y) + vector.type = .oneSidedArrow + } + } else if let sticker = entity as? DrawingStickerEntity { + sticker.position = center + sticker.rotation = rotation + if setup { + sticker.referenceDrawingSize = self.size + sticker.scale = zoomScale + } + } else if let bubble = entity as? DrawingBubbleEntity { + bubble.position = center + bubble.rotation = rotation + if setup { + let size = self.newEntitySize() + bubble.referenceDrawingSize = self.size + bubble.size = CGSize(width: size.width, height: round(size.height * 0.7)) + bubble.tailPosition = CGPoint(x: 0.16, y: size.height * 0.18) + } + } else if let text = entity as? DrawingTextEntity { + text.position = center + text.rotation = rotation + if setup { + text.referenceDrawingSize = self.size + text.width = floor(self.size.width * 0.9) + text.fontSize = 0.3 + text.scale = zoomScale + } + } + } + + @discardableResult + func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView { + let view = entity.makeView(context: self.context) + view.containerView = self + + view.onSnapToXAxis = { [weak self, weak view] snappedToX in + guard let strongSelf = self, let strongView = view else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if snappedToX { + strongSelf.insertSubview(strongSelf.xAxisView, belowSubview: strongView) + if strongSelf.xAxisView.alpha < 1.0 { + strongSelf.hapticFeedback.impact(.light) + } + transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 1.0) + } else { + transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 0.0) + } + } + view.onSnapToYAxis = { [weak self, weak view] snappedToY in + guard let strongSelf = self, let strongView = view else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if snappedToY { + strongSelf.insertSubview(strongSelf.yAxisView, belowSubview: strongView) + if strongSelf.yAxisView.alpha < 1.0 { + strongSelf.hapticFeedback.impact(.light) + } + transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 1.0) + } else { + transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 0.0) + } + } + view.onSnapToAngle = { [weak self, weak view] snappedToAngle in + guard let strongSelf = self, let strongView = view else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if let snappedToAngle { + strongSelf.layer.insertSublayer(strongSelf.angleLayer, below: strongView.layer) + strongSelf.angleLayer.transform = CATransform3DMakeRotation(snappedToAngle, 0.0, 0.0, 1.0) + if strongSelf.angleLayer.opacity < 1.0 { + strongSelf.hapticFeedback.impact(.light) + } + transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 1.0) + } else { + transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 0.0) + } + } + view.onPositionUpdated = { [weak self] position in + guard let strongSelf = self else { + return + } + strongSelf.angleLayer.position = position + } + + view.update() + self.addSubview(view) + + if announce { + self.entityAdded(entity) + } + return view + } + + func duplicate(_ entity: DrawingEntity) -> DrawingEntity { + let newEntity = entity.duplicate() + self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) + + let view = newEntity.makeView(context: self.context) + view.containerView = self + view.update() + self.addSubview(view) + return newEntity + } + + func remove(uuid: UUID, animated: Bool = false, announce: Bool = true) { + if let view = self.getView(for: uuid) { + if self.selectedEntityView === view { + self.selectedEntityView = nil + self.selectionChanged(nil) + self.hasSelectionChanged(false) + } + if animated { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + if !(view.entity is DrawingVectorEntity) { + view.layer.animateScale(from: view.entity.scale, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + if let selectionView = view.selectionView { + selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in + selectionView?.removeFromSuperview() + }) + if !(view.entity is DrawingVectorEntity) { + selectionView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + } + } else { + view.removeFromSuperview() + } + + if announce { + self.entityRemoved(view.entity) + } + } + } + + func removeAll() { + self.clear(animated: true) + self.selectionChanged(nil) + self.hasSelectionChanged(false) + } + + private func clear(animated: Bool = false) { + if animated { + for case let view as DrawingEntityView in self.subviews { + if let selectionView = view.selectionView { + selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak selectionView] _ in + selectionView?.removeFromSuperview() + }) + } + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + if !(view.entity is DrawingVectorEntity) { + view.layer.animateScale(from: 0.0, to: -0.99, duration: 0.2, removeOnCompletion: false, additive: true) + } + } + + } else { + for case let view as DrawingEntityView in self.subviews { + view.selectionView?.removeFromSuperview() + view.removeFromSuperview() + } + } + } + + func bringToFront(uuid: UUID) { + if let view = self.getView(for: uuid) { + self.bringSubviewToFront(view) + } + } + + func getView(for uuid: UUID) -> DrawingEntityView? { + for case let view as DrawingEntityView in self.subviews { + if view.entity.uuid == uuid { + return view + } + } + return nil + } + + public func play() { + for case let view as DrawingEntityView in self.subviews { + view.play() + } + } + + public func pause() { + for case let view as DrawingEntityView in self.subviews { + view.pause() + } + } + + public func seek(to timestamp: Double) { + for case let view as DrawingEntityView in self.subviews { + view.seek(to: timestamp) + } + } + + public func resetToStart() { + for case let view as DrawingEntityView in self.subviews { + view.resetToStart() + } + } + + public func updateVisibility(_ visibility: Bool) { + for case let view as DrawingEntityView in self.subviews { + view.updateVisibility(visibility) + } + } + + @objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) { + let location = gestureRecognzier.location(in: self) + + var intersectedViews: [DrawingEntityView] = [] + for case let view as DrawingEntityView in self.subviews { + if view.precisePoint(inside: self.convert(location, to: view)) { + intersectedViews.append(view) + } + } + + if let entityView = intersectedViews.last { + self.selectEntity(entityView.entity) + } + } + + func selectEntity(_ entity: DrawingEntity?) { + if entity !== self.selectedEntityView?.entity { + if let selectedEntityView = self.selectedEntityView { + if let textEntityView = selectedEntityView as? DrawingTextEntityView, textEntityView.isEditing { + if entity == nil { + textEntityView.endEditing() + } else { + return + } + } + + self.selectedEntityView = nil + if let selectionView = selectedEntityView.selectionView { + selectedEntityView.selectionView = nil + selectionView.removeFromSuperview() + } + } + } + + if let entity = entity, let entityView = self.getView(for: entity.uuid) { + self.selectedEntityView = entityView + + let selectionView = entityView.makeSelectionView() + selectionView.tapped = { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.requestedMenuForEntityView(entityView, strongSelf.subviews.last === entityView) + } + } + entityView.selectionView = selectionView + self.selectionContainerView?.addSubview(selectionView) + entityView.update() + } + + self.selectionChanged(self.selectedEntityView?.entity) + self.hasSelectionChanged(self.selectedEntityView != nil) + } + + var isTrackingAnyEntity: Bool { + for case let view as DrawingEntityView in self.subviews { + if view.isTracking { + return true + } + } + return false + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return super.point(inside: point, with: event) + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self { + return nil + } + if let result = result as? DrawingEntityView, !result.precisePoint(inside: self.convert(point, to: result)) { + return nil + } + return result + } + + public func clearSelection() { + self.selectEntity(nil) + } + + public func onZoom() { + self.selectedEntityView?.updateSelectionView() + } + + public var hasSelection: Bool { + return self.selectedEntityView != nil + } + + public func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + selectionView.handlePinch(gestureRecognizer) + } + } + + public func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + if let selectedEntityView = self.selectedEntityView, let selectionView = selectedEntityView.selectionView { + selectionView.handleRotate(gestureRecognizer) + } + } +} + +public class DrawingEntityView: UIView { + let context: AccountContext + let entity: DrawingEntity + var isTracking = false + + weak var selectionView: DrawingEntitySelectionView? + weak var containerView: DrawingEntitiesView? + + var onSnapToXAxis: (Bool) -> Void = { _ in } + var onSnapToYAxis: (Bool) -> Void = { _ in } + var onSnapToAngle: (CGFloat?) -> Void = { _ in } + var onPositionUpdated: (CGPoint) -> Void = { _ in } + + init(context: AccountContext, entity: DrawingEntity) { + self.context = context + self.entity = entity + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let selectionView = self.selectionView { + selectionView.removeFromSuperview() + } + } + + var selectionBounds: CGRect { + return self.bounds + } + + func play() { + + } + + func pause() { + + } + + func seek(to timestamp: Double) { + + } + + func resetToStart() { + + } + + func updateVisibility(_ visibility: Bool) { + + } + + func update(animated: Bool = false) { + self.updateSelectionView() + } + + func updateSelectionView() { + guard let selectionView = self.selectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: bounds.width * scale + selectionView.selectionInset * 2.0, height: bounds.height * scale + selectionView.selectionInset * 2.0)) + + self.popIdentityTransformForMeasurement() + } + + private var realTransform: CGAffineTransform? + func pushIdentityTransformForMeasurement() { + guard self.realTransform == nil else { + return + } + self.realTransform = self.transform + self.transform = .identity + } + + func popIdentityTransformForMeasurement() { + guard let realTransform = self.realTransform else { + return + } + self.transform = realTransform + self.realTransform = nil + } + + public func precisePoint(inside point: CGPoint) -> Bool { + return self.point(inside: point, with: nil) + } + + func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + return DrawingEntitySelectionView() + } +} + +let entitySelectionViewHandleSize = CGSize(width: 44.0, height: 44.0) +public class DrawingEntitySelectionView: UIView { + weak var entityView: DrawingEntityView? + + var tapped: () -> Void = { } + + override init(frame: CGRect) { + super.init(frame: frame) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.tapped() + } + + @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + } + + @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + } + + var selectionInset: CGFloat { + return 0.0 + } +} + +public class DrawingSelectionContainerView: UIView { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self { + return nil + } + return result + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let result = super.point(inside: point, with: event) + if !result { + for subview in self.subviews { + let subpoint = self.convert(point, to: subview) + if subview.point(inside: subpoint, with: event) { + return true + } + } + } + return result + } +} diff --git a/submodules/DrawingUI/Sources/DrawingGesture.swift b/submodules/DrawingUI/Sources/DrawingGesture.swift new file mode 100644 index 00000000000..d167a698546 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingGesture.swift @@ -0,0 +1,93 @@ +import Foundation +import UIKit + +class DrawingGestureRecognizer: UIPanGestureRecognizer { + var shouldBegin: (CGPoint) -> Bool = { _ in return true } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + if touches.count == 1, let touch = touches.first, self.shouldBegin(touch.location(in: self.view)) { + super.touchesBegan(touches, with: event) + self.state = .began + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + if touches.count > 1 { + self.state = .cancelled + } else { + super.touchesMoved(touches, with: event) + } + } +} + +struct DrawingPoint { + let location: CGPoint + let velocity: CGFloat + + var x: CGFloat { + return self.location.x + } + + var y: CGFloat { + return self.location.y + } +} + +class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate { + enum DrawingGestureState { + case began + case changed + case ended + case cancelled + } + + var onDrawing: (DrawingGestureState, DrawingPoint) -> Void = { _, _ in } + + var gestureRecognizer: DrawingGestureRecognizer? + var transform: CGAffineTransform = .identity + + init(view: DrawingView) { + super.init() + + let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:))) + gestureRecognizer.delegate = self + self.gestureRecognizer = gestureRecognizer + view.addGestureRecognizer(gestureRecognizer) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPinchGestureRecognizer { + return true + } + return false + } + + var previousPoint: DrawingPoint? + @objc private func handleGesture(_ gestureRecognizer: DrawingGestureRecognizer) { + let state: DrawingGestureState + switch gestureRecognizer.state { + case .began: + state = .began + case .changed: + state = .changed + case .ended: + state = .ended + case .cancelled: + state = .cancelled + case .failed: + state = .cancelled + case .possible: + state = .cancelled + @unknown default: + state = .cancelled + } + + let originalLocation = gestureRecognizer.location(in: gestureRecognizer.view) + let location = originalLocation.applying(self.transform) + let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view).applying(self.transform) + let velocityValue = velocity.length + + let point = DrawingPoint(location: location, velocity: velocityValue) + self.onDrawing(state, point) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingMetalView.swift b/submodules/DrawingUI/Sources/DrawingMetalView.swift new file mode 100644 index 00000000000..34c7e6742c7 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingMetalView.swift @@ -0,0 +1,713 @@ +import Foundation +import UIKit +import QuartzCore +import MetalKit +import Display +import SwiftSignalKit +import AppBundle + +final class DrawingMetalView: MTKView { + let size: CGSize + + private let commandQueue: MTLCommandQueue + fileprivate let library: MTLLibrary + private var pipelineState: MTLRenderPipelineState! + + fileprivate var drawable: Drawable? + + private var render_target_vertex: MTLBuffer! + private var render_target_uniform: MTLBuffer! + + private var markerBrush: Brush? + + init?(size: CGSize) { + let mainBundle = Bundle(for: DrawingView.self) + guard let path = mainBundle.path(forResource: "DrawingUIBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + guard let device = MTLCreateSystemDefaultDevice() else { + return nil + } + guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + return nil + } + self.library = defaultLibrary + + guard let commandQueue = device.makeCommandQueue() else { + return nil + } + self.commandQueue = commandQueue + + self.size = size + + super.init(frame: CGRect(origin: .zero, size: size), device: device) + + self.drawableSize = self.size + self.autoResizeDrawable = false + self.isOpaque = false + self.contentScaleFactor = 1.0 + self.isPaused = true + self.preferredFramesPerSecond = 60 + self.presentsWithTransaction = true + self.clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + + self.setup() + } + + override var isHidden: Bool { + didSet { + if self.isHidden { + Queue.mainQueue().after(0.2) { + self.isPaused = true + } + } else { + self.isPaused = self.isHidden + } + } + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeTexture(with data: Data) -> MTLTexture? { + let textureLoader = MTKTextureLoader(device: device!) + return try? textureLoader.newTexture(data: data, options: [.SRGB : false]) + } + + func makeTexture(with image: UIImage) -> MTLTexture? { + if let data = image.pngData() { + return makeTexture(with: data) + } else { + return nil + } + } + + func drawInContext(_ cgContext: CGContext) { + guard let texture = self.drawable?.texture, let image = texture.createCGImage() else { + return + } + let rect = CGRect(origin: .zero, size: CGSize(width: image.width, height: image.height)) + cgContext.saveGState() + cgContext.translateBy(x: rect.midX, y: rect.midY) + cgContext.scaleBy(x: 1.0, y: -1.0) + cgContext.translateBy(x: -rect.midX, y: -rect.midY) + cgContext.draw(image, in: rect) + cgContext.restoreGState() + } + + private func setup() { + self.drawable = Drawable(size: self.size, pixelFormat: self.colorPixelFormat, device: device) + + let size = self.size + let w = size.width, h = size.height + let vertices = [ + Vertex(position: CGPoint(x: 0 , y: 0), texCoord: CGPoint(x: 0, y: 0)), + Vertex(position: CGPoint(x: w , y: 0), texCoord: CGPoint(x: 1, y: 0)), + Vertex(position: CGPoint(x: 0 , y: h), texCoord: CGPoint(x: 0, y: 1)), + Vertex(position: CGPoint(x: w , y: h), texCoord: CGPoint(x: 1, y: 1)), + ] + self.render_target_vertex = self.device?.makeBuffer(bytes: vertices, length: MemoryLayout.stride * vertices.count, options: .cpuCacheModeWriteCombined) + + let matrix = Matrix.identity + matrix.scaling(x: 2.0 / Float(size.width), y: -2.0 / Float(size.height), z: 1) + matrix.translation(x: -1, y: 1, z: 0) + self.render_target_uniform = self.device?.makeBuffer(bytes: matrix.m, length: MemoryLayout.size * 16, options: []) + + let vertexFunction = self.library.makeFunction(name: "vertex_render_target") + let fragmentFunction = self.library.makeFunction(name: "fragment_render_target") + let pipelineDescription = MTLRenderPipelineDescriptor() + pipelineDescription.vertexFunction = vertexFunction + pipelineDescription.fragmentFunction = fragmentFunction + pipelineDescription.colorAttachments[0].pixelFormat = colorPixelFormat + + do { + self.pipelineState = try self.device?.makeRenderPipelineState(descriptor: pipelineDescription) + } catch { + fatalError(error.localizedDescription) + } + + if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) { + self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(-0.55)) + } + + self.drawable?.clear() + + Queue.mainQueue().after(0.1) { + self.markerBrush?.pushPoint(CGPoint(x: 100.0, y: 100.0), color: DrawingColor.clear, size: 0.0, isEnd: true) + Queue.mainQueue().after(0.1) { + self.clear() + } + } + } + + override var frame: CGRect { + get { + return super.frame + } set { + super.frame = newValue + self.drawableSize = self.size + } + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + guard let drawable = self.drawable, let texture = drawable.texture?.texture else { + return + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + let attachment = renderPassDescriptor.colorAttachments[0] + attachment?.clearColor = self.clearColor + attachment?.texture = self.currentDrawable?.texture + attachment?.loadAction = .clear + attachment?.storeAction = .store + + guard let _ = attachment?.texture else { + return + } + + let commandBuffer = self.commandQueue.makeCommandBuffer() + let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + + commandEncoder?.setRenderPipelineState(self.pipelineState) + + commandEncoder?.setVertexBuffer(self.render_target_vertex, offset: 0, index: 0) + commandEncoder?.setVertexBuffer(self.render_target_uniform, offset: 0, index: 1) + commandEncoder?.setFragmentTexture(texture, index: 0) + commandEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + + commandEncoder?.endEncoding() + commandBuffer?.commit() + commandBuffer?.waitUntilScheduled() + self.currentDrawable?.present() + } + + func reset() { + let renderPassDescriptor = MTLRenderPassDescriptor() + let attachment = renderPassDescriptor.colorAttachments[0] + attachment?.clearColor = self.clearColor + attachment?.texture = self.currentDrawable?.texture + attachment?.loadAction = .clear + attachment?.storeAction = .store + + let commandBuffer = self.commandQueue.makeCommandBuffer() + let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + + commandEncoder?.endEncoding() + commandBuffer?.commit() + commandBuffer?.waitUntilScheduled() + self.currentDrawable?.present() + } + + func clear() { + guard let drawable = self.drawable else { + return + } + + drawable.updateBuffer(with: self.size) + drawable.clear() + self.reset() + } + + enum BrushType { + case marker + } + + func updated(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, brush: BrushType, color: DrawingColor, size: CGFloat) { + switch brush { + case .marker: + self.markerBrush?.updated(point, color: color, state: state, size: size) + } + } +} + +private class Drawable { + public private(set) var texture: Texture? + + internal var pixelFormat: MTLPixelFormat = .bgra8Unorm + internal var size: CGSize + internal var uniform_buffer: MTLBuffer! + internal var renderPassDescriptor: MTLRenderPassDescriptor? + internal var commandBuffer: MTLCommandBuffer? + internal var commandQueue: MTLCommandQueue? + internal var device: MTLDevice? + + public init(size: CGSize, pixelFormat: MTLPixelFormat, device: MTLDevice?) { + self.size = size + self.pixelFormat = pixelFormat + self.device = device + self.texture = self.makeTexture() + self.commandQueue = device?.makeCommandQueue() + + self.renderPassDescriptor = MTLRenderPassDescriptor() + let attachment = self.renderPassDescriptor?.colorAttachments[0] + attachment?.texture = self.texture?.texture + attachment?.loadAction = .load + attachment?.storeAction = .store + + self.updateBuffer(with: size) + } + + func clear() { + self.texture?.clear() + } + + func reset() { + self.prepareForDraw() + + if let commandEncoder = self.makeCommandEncoder() { + commandEncoder.endEncoding() + } + + self.commit(wait: true) + } + + internal func updateBuffer(with size: CGSize) { + self.size = size + + let matrix = Matrix.identity + self.uniform_buffer = device?.makeBuffer(bytes: matrix.m, length: MemoryLayout.size * 16, options: []) + } + + internal func prepareForDraw() { + if self.commandBuffer == nil { + self.commandBuffer = self.commandQueue?.makeCommandBuffer() + } + } + + internal func makeCommandEncoder() -> MTLRenderCommandEncoder? { + guard let commandBuffer = self.commandBuffer, let renderPassDescriptor = self.renderPassDescriptor else { + return nil + } + return commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) + } + + + internal func commit(wait: Bool = false) { + self.commandBuffer?.commit() + if wait { + self.commandBuffer?.waitUntilCompleted() + } + self.commandBuffer = nil + } + + internal func makeTexture() -> Texture? { + guard self.size.width * self.size.height > 0, let device = self.device else { + return nil + } + return Texture(device: device, width: Int(self.size.width), height: Int(self.size.height)) + } +} + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +private class Brush { + private(set) var texture: MTLTexture? + private(set) var pipelineState: MTLRenderPipelineState! + + weak var target: DrawingMetalView? + + public enum Rotation { + case fixed(CGFloat) + case random + case ahead + } + + var rotation: Rotation + + required public init(texture: MTLTexture?, target: DrawingMetalView, rotation: Rotation) { + self.texture = texture + self.target = target + self.rotation = rotation + + self.setupPipeline() + } + + private func setupPipeline() { + guard let target = self.target, let device = target.device else { + return + } + + let renderPipelineDescriptor = MTLRenderPipelineDescriptor() + if let vertex_func = target.library.makeFunction(name: "vertex_point_func") { + renderPipelineDescriptor.vertexFunction = vertex_func + } + if let _ = self.texture { + if let fragment_func = target.library.makeFunction(name: "fragment_point_func") { + renderPipelineDescriptor.fragmentFunction = fragment_func + } + } else { + if let fragment_func = target.library.makeFunction(name: "fragment_point_func_without_texture") { + renderPipelineDescriptor.fragmentFunction = fragment_func + } + } + renderPipelineDescriptor.colorAttachments[0].pixelFormat = target.colorPixelFormat + + let attachment = renderPipelineDescriptor.colorAttachments[0] + attachment?.isBlendingEnabled = true + + attachment?.rgbBlendOperation = .add + attachment?.sourceRGBBlendFactor = .sourceAlpha + attachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha + + attachment?.alphaBlendOperation = .add + attachment?.sourceAlphaBlendFactor = .one + attachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha + + self.pipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) + } + + func render(stroke: Stroke, in drawable: Drawable? = nil) { + let drawable = drawable ?? target?.drawable + + guard stroke.lines.count > 0, let target = drawable else { + return + } + + target.prepareForDraw() + + let commandEncoder = target.makeCommandEncoder() + commandEncoder?.setRenderPipelineState(self.pipelineState) + + if let vertex_buffer = stroke.preparedBuffer(rotation: self.rotation) { + commandEncoder?.setVertexBuffer(vertex_buffer, offset: 0, index: 0) + commandEncoder?.setVertexBuffer(target.uniform_buffer, offset: 0, index: 1) + if let texture = texture { + commandEncoder?.setFragmentTexture(texture, index: 0) + } + commandEncoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: stroke.vertexCount) + } + + commandEncoder?.endEncoding() + } + + private let bezier = BezierGenerator() + func updated(_ point: DrawingPoint, color: DrawingColor, state: DrawingGesturePipeline.DrawingGestureState, size: CGFloat) { + let point = point.location + switch state { + case .began: + self.bezier.begin(with: point) + let _ = self.pushPoint(point, color: color, size: size, isEnd: false) + case .changed: + if self.bezier.points.count > 0 && point != lastRenderedPoint { + self.pushPoint(point, color: color, size: size, isEnd: false) + } + case .ended, .cancelled: + if self.bezier.points.count >= 3 { + self.pushPoint(point, color: color, size: size, isEnd: true) + } + self.bezier.finish() + self.lastRenderedPoint = nil + } + } + + func setup(_ inputPoints: [CGPoint], color: DrawingColor, size: CGFloat) { + guard inputPoints.count >= 2 else { + return + } + var pointStep: CGFloat + if case .random = self.rotation { + pointStep = size * 0.1 + } else { + pointStep = 2.0 + } + + var lines: [Line] = [] + + var previousPoint = inputPoints[0] + + var points: [CGPoint] = [] + self.bezier.begin(with: inputPoints.first!) + for point in inputPoints { + let smoothPoints = self.bezier.pushPoint(point) + points.append(contentsOf: smoothPoints) + } + self.bezier.finish() + + guard points.count >= 2 else { + return + } + for i in 1 ..< points.count { + let p = points[i] + if (i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) { + let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep) + lines.append(line) + previousPoint = p + } + } + + if let drawable = self.target?.drawable { + let stroke = Stroke(color: color, lines: lines, target: drawable) + self.render(stroke: stroke, in: drawable) + drawable.commit(wait: true) + } + } + + private var lastRenderedPoint: CGPoint? + func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) { + var pointStep: CGFloat + if case .random = self.rotation { + pointStep = size * 0.1 + } else { + pointStep = 2.0 + } + + var lines: [Line] = [] + let points = self.bezier.pushPoint(point) + guard points.count >= 2 else { + return + } + var previousPoint = self.lastRenderedPoint ?? points[0] + for i in 1 ..< points.count { + let p = points[i] + if (isEnd && i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) { + let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep) + lines.append(line) + previousPoint = p + } + } + + if let drawable = self.target?.drawable { + let stroke = Stroke(color: color, lines: lines, target: drawable) + self.render(stroke: stroke, in: drawable) + drawable.commit() + } + } +} + +private class Stroke { + private weak var target: Drawable? + + let color: DrawingColor + var lines: [Line] = [] + + private(set) var vertexCount: Int = 0 + private var vertex_buffer: MTLBuffer? + + init(color: DrawingColor, lines: [Line] = [], target: Drawable) { + self.color = color + self.lines = lines + self.target = target + + let _ = self.preparedBuffer(rotation: .fixed(0)) + } + + func append(_ lines: [Line]) { + self.lines.append(contentsOf: lines) + self.vertex_buffer = nil + } + + func preparedBuffer(rotation: Brush.Rotation) -> MTLBuffer? { + guard !self.lines.isEmpty else { + return nil + } + + var vertexes: [Point] = [] + + self.lines.forEach { (line) in + let count = max(line.length / line.pointStep, 1) + + let overlapping = max(1, line.pointSize / line.pointStep) + var renderingColor = self.color + renderingColor.alpha = renderingColor.alpha / overlapping * 5.5 + + for i in 0 ..< Int(count) { + let index = CGFloat(i) + let x = line.start.x + (line.end.x - line.start.x) * (index / count) + let y = line.start.y + (line.end.y - line.start.y) * (index / count) + + var angle: CGFloat = 0 + switch rotation { + case let .fixed(a): + angle = a + case .random: + angle = CGFloat.random(in: -CGFloat.pi ... CGFloat.pi) + case .ahead: + angle = line.angle + } + + vertexes.append(Point(x: x, y: y, color: renderingColor, size: line.pointSize, angle: angle)) + } + } + + self.vertexCount = vertexes.count + self.vertex_buffer = self.target?.device?.makeBuffer(bytes: vertexes, length: MemoryLayout.stride * vertexCount, options: .cpuCacheModeWriteCombined) + + return self.vertex_buffer + } +} + +class BezierGenerator { + init() { + } + + init(beginPoint: CGPoint) { + self.begin(with: beginPoint) + } + + func begin(with point: CGPoint) { + self.step = 0 + self.points.removeAll() + self.points.append(point) + } + + func pushPoint(_ point: CGPoint) -> [CGPoint] { + if point == self.points.last { + return [] + } + self.points.append(point) + if self.points.count < 3 { + return [] + } + self.step += 1 + return self.generateSmoothPathPoints() + } + + func finish() { + self.step = 0 + self.points.removeAll() + } + + var points: [CGPoint] = [] + + private var step = 0 + private func generateSmoothPathPoints() -> [CGPoint] { + var begin: CGPoint + var control: CGPoint + let end = CGPoint.middle(p1: self.points[step], p2: self.points[self.step + 1]) + + var vertices: [CGPoint] = [] + if self.step == 1 { + begin = self.points[0] + let middle1 = CGPoint.middle(p1: self.points[0], p2: self.points[1]) + control = CGPoint.middle(p1: middle1, p2: self.points[1]) + } else { + begin = CGPoint.middle(p1: self.points[self.step - 1], p2: self.points[self.step]) + control = self.points[self.step] + } + + let distance = begin.distance(to: end) + let segements = max(Int(distance / 5), 2) + + for i in 0 ..< segements { + let t = CGFloat(i) / CGFloat(segements) + vertices.append(begin.quadBezierPoint(to: end, controlPoint: control, t: t)) + } + vertices.append(end) + return vertices + } +} + +private struct Line { + var start: CGPoint + var end: CGPoint + + var pointSize: CGFloat + var pointStep: CGFloat + + init(start: CGPoint, end: CGPoint, pointSize: CGFloat, pointStep: CGFloat) { + self.start = start + self.end = end + self.pointSize = pointSize + self.pointStep = pointStep + } + + var length: CGFloat { + return self.start.distance(to: self.end) + } + + var angle: CGFloat { + return self.end.angle(to: self.start) + } +} + +final class Texture { + let buffer: MTLBuffer? + + let width: Int + let height: Int + let bytesPerRow: Int + let texture: MTLTexture + + init?(device: MTLDevice, width: Int, height: Int) { + let bytesPerPixel = 4 + let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm) + let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment) + + self.width = width + self.height = height + self.bytesPerRow = bytesPerRow + + self.buffer = nil + + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.pixelFormat = .bgra8Unorm + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.usage = [.renderTarget, .shaderRead] + textureDescriptor.storageMode = .shared + + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return nil + } + + self.texture = texture + + self.clear() + } + + func clear() { + let region = MTLRegion( + origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize(width: self.width, height: self.height, depth: 1) + ) + let data = Data(capacity: Int(self.bytesPerRow * self.height)) + if let bytes = data.withUnsafeBytes({ $0.baseAddress }) { + self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: self.bytesPerRow) + } + } + + func createCGImage() -> CGImage? { + let dataProvider: CGDataProvider + + guard let data = NSMutableData(capacity: self.bytesPerRow * self.height) else { + return nil + } + data.length = self.bytesPerRow * self.height + self.texture.getBytes(data.mutableBytes, bytesPerRow: self.bytesPerRow, bytesPerImage: self.bytesPerRow * self.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.width, height: self.height, depth: 1)), mipmapLevel: 0, slice: 0) + + guard let provider = CGDataProvider(data: data as CFData) else { + return nil + } + dataProvider = provider + + guard let image = CGImage( + width: Int(self.width), + height: Int(self.height), + bitsPerComponent: 8, + bitsPerPixel: 8 * 4, + bytesPerRow: self.bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) else { + return nil + } + + return image + } +} diff --git a/submodules/DrawingUI/Sources/DrawingNeonTool.swift b/submodules/DrawingUI/Sources/DrawingNeonTool.swift new file mode 100644 index 00000000000..4a3f5aaad35 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingNeonTool.swift @@ -0,0 +1,263 @@ +import Foundation +import UIKit +import Display + +final class NeonTool: DrawingElement { + class RenderView: UIView, DrawingRenderView { + private weak var element: NeonTool? + private var drawScale = CGSize(width: 1.0, height: 1.0) + + let shadowLayer = SimpleShapeLayer() + let borderLayer = SimpleShapeLayer() + let fillLayer = SimpleShapeLayer() + + func setup(element: NeonTool, size: CGSize, screenSize: CGSize) { + self.element = element + + self.backgroundColor = .clear + self.isOpaque = false + self.contentScaleFactor = 1.0 + + let shadowRadius = element.renderShadowRadius + let strokeWidth = element.renderStrokeWidth + var shadowColor = element.color.toUIColor() + var fillColor: UIColor = .white + if shadowColor.lightness < 0.01 { + fillColor = shadowColor + shadowColor = UIColor(rgb: 0x440881) + } + + let bounds = CGRect(origin: .zero, size: size) + self.frame = bounds + + self.shadowLayer.frame = bounds + self.shadowLayer.contentsScale = 1.0 + self.shadowLayer.backgroundColor = UIColor.clear.cgColor + self.shadowLayer.lineWidth = strokeWidth * 0.5 + self.shadowLayer.lineCap = .round + self.shadowLayer.lineJoin = .round + self.shadowLayer.fillColor = fillColor.cgColor + self.shadowLayer.strokeColor = fillColor.cgColor + self.shadowLayer.shadowColor = shadowColor.cgColor + self.shadowLayer.shadowRadius = shadowRadius + self.shadowLayer.shadowOpacity = 1.0 + self.shadowLayer.shadowOffset = .zero + + self.borderLayer.frame = bounds + self.borderLayer.contentsScale = 1.0 + self.borderLayer.lineWidth = strokeWidth + self.borderLayer.lineCap = .round + self.borderLayer.lineJoin = .round + self.borderLayer.fillColor = UIColor.clear.cgColor + self.borderLayer.strokeColor = fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor + + self.fillLayer.frame = bounds + self.fillLayer.contentsScale = 1.0 + self.fillLayer.fillColor = fillColor.cgColor + + self.layer.addSublayer(self.shadowLayer) + self.layer.addSublayer(self.borderLayer) + self.layer.addSublayer(self.fillLayer) + } + + fileprivate func updatePath(_ path: CGPath) { + self.shadowLayer.path = path + self.borderLayer.path = path + self.fillLayer.path = path + } + } + + let uuid: UUID + let drawingSize: CGSize + let color: DrawingColor + let renderStrokeWidth: CGFloat + let renderShadowRadius: CGFloat + let renderLineWidth: CGFloat + let renderColor: UIColor + + private var pathStarted = false + private let path = UIBezierPath() + private var activePath: UIBezierPath? + private var addedPaths = 0 + + fileprivate var renderPath: CGPath? + + var translation: CGPoint = .zero + + private weak var currentRenderView: DrawingRenderView? + + var isValid: Bool { + return self.renderPath != nil + } + + var bounds: CGRect { + if let renderPath = self.renderPath { + return normalizeDrawingRect(renderPath.boundingBoxOfPath.insetBy(dx: -self.renderShadowRadius - 30.0, dy: -self.renderShadowRadius - 30.0), drawingSize: self.drawingSize) + } else { + return .zero + } + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.drawingSize = drawingSize + self.color = color + + let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.01 + let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03 + + let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002) + let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderStrokeWidth = strokeWidth + self.renderShadowRadius = shadowRadius + self.renderLineWidth = lineWidth + + self.renderColor = color.withUpdatedAlpha(1.0).toUIColor() + } + + func setupRenderView(screenSize: CGSize) -> DrawingRenderView? { + let view = RenderView() + view.setup(element: self, size: self.drawingSize, screenSize: screenSize) + self.currentRenderView = view + return view + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) { + guard self.addPoint(point, state: state, zoomScale: zoomScale) || state == .ended else { + return + } + + if let currentRenderView = self.currentRenderView as? RenderView { + let path = self.path.cgPath.mutableCopy() + if let activePath { + path?.addPath(activePath.cgPath) + } + if let renderPath = path?.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) { + self.renderPath = renderPath + currentRenderView.updatePath(renderPath) + } + } + + if state == .ended { + if let activePath = self.activePath { + self.path.append(activePath) + self.renderPath = self.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0) + } else if self.addedPaths == 0, let point = self.points.first { + self.renderPath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: point.x - self.renderLineWidth / 2.0, y: point.y - self.renderLineWidth / 2.0), size: CGSize(width: self.renderLineWidth, height: self.renderLineWidth)), transform: nil) + } + } + } + + func draw(in context: CGContext, size: CGSize) { + guard let path = self.renderPath else { + return + } + context.saveGState() + + context.translateBy(x: self.translation.x, y: self.translation.y) + + context.setShouldAntialias(true) + + context.setBlendMode(.normal) + + var shadowColor = self.color.toUIColor() + var fillColor: UIColor = .white + if shadowColor.lightness < 0.01 { + fillColor = shadowColor + shadowColor = UIColor(rgb: 0x440881) + } + + context.addPath(path) + context.setLineCap(.round) + context.setFillColor(fillColor.cgColor) + context.setStrokeColor(fillColor.cgColor) + context.setLineWidth(self.renderStrokeWidth * 0.5) + context.setShadow(offset: .zero, blur: self.renderShadowRadius * 1.9, color: shadowColor.cgColor) + context.drawPath(using: .fillStroke) + + context.addPath(path) + context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor) + context.setLineWidth(self.renderStrokeWidth) + context.setStrokeColor(fillColor.mixedWith(shadowColor, alpha: 0.25).cgColor) + context.strokePath() + + context.addPath(path) + context.setFillColor(fillColor.cgColor) + + context.fillPath() + + context.restoreGState() + } + + private var points: [CGPoint] = Array(repeating: .zero, count: 4) + private var pointPtr = 0 + + private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> Bool { + let filterDistance: CGFloat = 10.0 / zoomScale + + if self.pointPtr == 0 { + self.points[0] = point.location + self.pointPtr += 1 + } else { + let previousPoint = self.points[self.pointPtr - 1] + guard previousPoint.distance(to: point.location) > filterDistance else { + return false + } + + if self.pointPtr >= 4 { + self.points[3] = self.points[2].point(to: point.location, t: 0.5) + + if let bezierPath = self.currentBezierPath(3) { + self.path.append(bezierPath) + self.addedPaths += 1 + self.activePath = nil + } + + self.points[0] = self.points[3] + self.pointPtr = 1 + } + + self.points[self.pointPtr] = point.location + self.pointPtr += 1 + } + + guard let bezierPath = self.currentBezierPath(self.pointPtr - 1) else { + return false + } + + self.activePath = bezierPath + + return true + } + + private func currentBezierPath(_ ctr: Int) -> UIBezierPath? { + switch ctr { + case 0: + return nil + case 1: + let path = UIBezierPath() + path.move(to: self.points[0]) + path.addLine(to: self.points[1]) + return path + case 2: + let path = UIBezierPath() + path.move(to: self.points[0]) + path.addQuadCurve(to: self.points[2], controlPoint: self.points[1]) + return path + case 3: + let path = UIBezierPath() + path.move(to: self.points[0]) + path.addCurve(to: self.points[3], controlPoint1: self.points[1], controlPoint2: self.points[2]) + return path + default: + return nil + } + } +} + diff --git a/submodules/DrawingUI/Sources/DrawingPenTool.swift b/submodules/DrawingUI/Sources/DrawingPenTool.swift new file mode 100644 index 00000000000..75a30bbc0d6 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingPenTool.swift @@ -0,0 +1,745 @@ +import Foundation +import UIKit +import Display + +final class PenTool: DrawingElement { + class RenderView: UIView, DrawingRenderView { + private weak var element: PenTool? + private var isEraser = false + + private var accumulationImage: UIImage? + private var activeView: ActiveView? + + private var start = 0 + private var segmentsCount = 0 + + private var drawScale = CGSize(width: 1.0, height: 1.0) + + func setup(size: CGSize, screenSize: CGSize, isEraser: Bool) { + self.isEraser = isEraser + + self.backgroundColor = .clear + self.isOpaque = false + self.contentMode = .redraw + + //let scale = CGSize(width: screenSize.width / max(1.0, size.width), height: screenSize.height / max(1.0, size.height)) + let scale = CGSize(width: 0.33, height: 0.33) + let viewSize = CGSize(width: size.width * scale.width, height: size.height * scale.height) + + self.drawScale = CGSize(width: size.width / viewSize.width, height: size.height / viewSize.height) + + self.bounds = CGRect(origin: .zero, size: viewSize) + self.transform = CGAffineTransform(scaleX: self.drawScale.width, y: self.drawScale.height) + self.frame = CGRect(origin: .zero, size: size) + + self.drawScale.height = self.drawScale.width + + let activeView = ActiveView(frame: CGRect(origin: .zero, size: self.bounds.size)) + activeView.backgroundColor = .clear + activeView.contentMode = .redraw + activeView.isOpaque = false + activeView.parent = self + self.addSubview(activeView) + self.activeView = activeView + } + + func animateArrowPaths(start: CGPoint, direction: CGFloat, length: CGFloat, lineWidth: CGFloat, completion: @escaping () -> Void) { + let scale = min(self.drawScale.width, self.drawScale.height) + + let arrowStart = CGPoint(x: start.x / scale, y: start.y / scale) + let arrowLeftPath = UIBezierPath() + arrowLeftPath.move(to: arrowStart) + arrowLeftPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction - 0.45)) + + let arrowRightPath = UIBezierPath() + arrowRightPath.move(to: arrowStart) + arrowRightPath.addLine(to: arrowStart.pointAt(distance: length / scale, angle: direction + 0.45)) + + let leftArrowShape = CAShapeLayer() + leftArrowShape.path = arrowLeftPath.cgPath + leftArrowShape.lineWidth = lineWidth / scale + leftArrowShape.strokeColor = self.element?.color.toCGColor() + leftArrowShape.lineCap = .round + leftArrowShape.frame = self.bounds + self.layer.addSublayer(leftArrowShape) + + let rightArrowShape = CAShapeLayer() + rightArrowShape.path = arrowRightPath.cgPath + rightArrowShape.lineWidth = lineWidth / scale + rightArrowShape.strokeColor = self.element?.color.toCGColor() + rightArrowShape.lineCap = .round + rightArrowShape.frame = self.bounds + self.layer.addSublayer(rightArrowShape) + + leftArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + rightArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, completion: { [weak leftArrowShape, weak rightArrowShape] _ in + completion() + + leftArrowShape?.removeFromSuperlayer() + rightArrowShape?.removeFromSuperlayer() + }) + } + + var displaySize: CGSize? + fileprivate func draw(element: PenTool, rect: CGRect) { + self.element = element + + self.alpha = element.color.alpha + + guard !rect.isInfinite && !rect.isEmpty && !rect.isNull else { + return + } + + var rect: CGRect? = rect + + let limit = 512 + let activeCount = self.segmentsCount - self.start + if activeCount > limit { + rect = nil + let newStart = self.start + limit + let displaySize = self.displaySize ?? CGSize(width: round(self.bounds.size.width), height: round(self.bounds.size.height)) + let image = generateImage(displaySize, contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let accumulationImage = self.accumulationImage, let cgImage = accumulationImage.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.scaleBy(x: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height) + + context.setBlendMode(.copy) + element.drawSegments(in: context, from: self.start, to: newStart) + }, opaque: false) + self.accumulationImage = image + self.layer.contents = image?.cgImage + + self.start = newStart + } + + self.segmentsCount = element.segments.count + + if let rect = rect { + self.activeView?.setNeedsDisplay(rect.insetBy(dx: -10.0, dy: -10.0).applying(CGAffineTransform(scaleX: 1.0 / self.drawScale.width, y: 1.0 / self.drawScale.height))) + } else { + self.activeView?.setNeedsDisplay() + } + } + + class ActiveView: UIView { + weak var parent: RenderView? + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext(), let parent = self.parent, let element = parent.element else { + return + } + + parent.displaySize = rect.size + context.scaleBy(x: 1.0 / parent.drawScale.width, y: 1.0 / parent.drawScale.height) + element.drawSegments(in: context, from: parent.start, to: parent.segmentsCount) + element.drawActiveSegments(in: context) + } + } + } + + let uuid: UUID + let drawingSize: CGSize + let color: DrawingColor + let renderLineWidth: CGFloat + let renderMinLineWidth: CGFloat + let renderColor: UIColor + + let hasArrow: Bool + let renderArrowLength: CGFloat + var renderArrowLineWidth: CGFloat + + let isEraser: Bool + + let isBlur: Bool + + var arrowStart: CGPoint? + var arrowDirection: CGFloat? + var arrowLeftPath: UIBezierPath? + var arrowRightPath: UIBezierPath? + + var translation: CGPoint = .zero + + var blurredImage: UIImage? + + private weak var currentRenderView: DrawingRenderView? + + var isValid: Bool { + if self.hasArrow { + return self.arrowStart != nil && self.arrowDirection != nil + } else { + return self.segments.count > 0 + } + } + + var bounds: CGRect { + return normalizeDrawingRect(boundingRect(from: 0, to: self.segments.count).insetBy(dx: -20.0, dy: -20.0), drawingSize: self.drawingSize) + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, hasArrow: Bool, isEraser: Bool, isBlur: Bool, blurredImage: UIImage?) { + self.uuid = UUID() + self.drawingSize = drawingSize + self.color = isEraser || isBlur ? DrawingColor(rgb: 0x000000) : color + self.hasArrow = hasArrow + self.isEraser = isEraser + self.isBlur = isBlur + self.blurredImage = blurredImage + + let minLineWidth = max(1.0, max(drawingSize.width, drawingSize.height) * 0.002) + let maxLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.07) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + let minRenderArrowLength = max(10.0, max(drawingSize.width, drawingSize.height) * 0.02) + + self.renderLineWidth = lineWidth + self.renderMinLineWidth = isEraser || isBlur ? lineWidth : minLineWidth + (lineWidth - minLineWidth) * 0.2 + self.renderArrowLength = max(minRenderArrowLength, lineWidth * 3.0) + self.renderArrowLineWidth = max(minLineWidth * 1.8, lineWidth * 0.75) + + self.renderColor = color.withUpdatedAlpha(1.0).toUIColor() + } + + var isFinishingArrow = false + func finishArrow(_ completion: @escaping () -> Void) { + if let arrowStart, let arrowDirection { + self.isFinishingArrow = true + (self.currentRenderView as? RenderView)?.animateArrowPaths(start: arrowStart, direction: arrowDirection, length: self.renderArrowLength, lineWidth: self.renderArrowLineWidth, completion: { [weak self] in + self?.isFinishingArrow = false + completion() + }) + } else { + completion() + } + } + + func setupRenderView(screenSize: CGSize) -> DrawingRenderView? { + let view = RenderView() + view.setup(size: self.drawingSize, screenSize: screenSize, isEraser: self.isEraser) + self.currentRenderView = view + return view + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) { + let result = self.addPoint(point, state: state, zoomScale: zoomScale) + let resetActiveRect = result?.0 ?? false + let updatedRect = result?.1 + var combinedRect = updatedRect + if let previousActiveRect = self.previousActiveRect { + combinedRect = updatedRect?.union(previousActiveRect) ?? previousActiveRect + } + if resetActiveRect { + self.previousActiveRect = updatedRect + } else { + self.previousActiveRect = combinedRect + } + + if let currentRenderView = self.currentRenderView as? RenderView, let combinedRect { + currentRenderView.draw(element: self, rect: combinedRect) + } + + if state == .ended { + if !self.activeSegments.isEmpty { + self.segments.append(contentsOf: self.activeSegments) + self.smoothPoints.append(contentsOf: self.activeSmoothPoints) + } + + if self.hasArrow { + var direction: CGFloat? + if self.smoothPoints.count > 4 { + let p2 = self.smoothPoints[self.smoothPoints.count - 1].location + for i in 1 ..< min(self.smoothPoints.count - 2, 200) { + let p1 = self.smoothPoints[self.smoothPoints.count - 1 - i].location + if p1.distance(to: p2) > self.renderArrowLength * 0.5 { + direction = p2.angle(to: p1) + break + } + } + } + + self.arrowStart = self.smoothPoints.last?.location + self.arrowDirection = direction + self.maybeSetupArrow() + } else if self.segments.isEmpty { + let radius = self.renderLineWidth / 2.0 + self.segments.append( + Segment( + a: CGPoint(x: point.x - radius, y: point.y), + b: CGPoint(x: point.x + radius, y: point.y), + c: CGPoint(x: point.x - radius, y: point.y + 0.1), + d: CGPoint(x: point.x + radius, y: point.y + 0.1), + radius1: radius, + radius2: radius, + rect: CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius * 2.0, height: radius * 2.0)) + ) + ) + } + } + } + + func maybeSetupArrow() { + if let start = self.arrowStart, let direction = self.arrowDirection { + let arrowLeftPath = UIBezierPath() + arrowLeftPath.move(to: start) + arrowLeftPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)) + + let arrowRightPath = UIBezierPath() + arrowRightPath.move(to: start) + arrowRightPath.addLine(to: start.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)) + + self.arrowLeftPath = arrowLeftPath + self.arrowRightPath = arrowRightPath + self.renderArrowLineWidth = self.smoothPoints.last?.width ?? self.renderArrowLineWidth + } + } + + func draw(in context: CGContext, size: CGSize) { + guard !self.segments.isEmpty else { + return + } + + context.saveGState() + + if self.isEraser { + context.setBlendMode(.clear) + } else if self.isBlur { + context.setBlendMode(.normal) + } else { + context.setAlpha(self.color.alpha) + context.setBlendMode(.copy) + } + + context.translateBy(x: self.translation.x, y: self.translation.y) + + context.setShouldAntialias(true) + + if self.isBlur, let blurredImage = self.blurredImage { + let maskContext = DrawingContext(size: size, scale: 0.5, clear: true) + maskContext?.withFlippedContext { maskContext in + self.drawSegments(in: maskContext, from: 0, to: self.segments.count) + } + if let maskImage = maskContext?.generateImage()?.cgImage, let blurredImage = blurredImage.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: maskImage) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(blurredImage, in: CGRect(origin: .zero, size: size)) + } + + } else { + self.drawSegments(in: context, from: 0, to: self.segments.count) + } + + if let arrowLeftPath, let arrowRightPath { + context.setStrokeColor(self.renderColor.cgColor) + context.setLineWidth(self.renderArrowLineWidth) + context.setLineCap(.round) + + context.addPath(arrowLeftPath.cgPath) + context.strokePath() + + context.addPath(arrowRightPath.cgPath) + context.strokePath() + } + + context.restoreGState() + + self.segmentPaths = [:] + } + + private struct Segment: Codable { + let a: CGPoint + let b: CGPoint + let c: CGPoint + let d: CGPoint + let radius1: CGFloat + let radius2: CGFloat + let rect: CGRect + + init( + a: CGPoint, + b: CGPoint, + c: CGPoint, + d: CGPoint, + radius1: CGFloat, + radius2: CGFloat, + rect: CGRect + ) { + self.a = a + self.b = b + self.c = c + self.d = d + self.radius1 = radius1 + self.radius2 = radius2 + self.rect = rect + } + } + + private struct Point { + let location: CGPoint + let width: CGFloat + + init( + location: CGPoint, + width: CGFloat + ) { + self.location = location + self.width = width + } + } + + private var points: [Point] = Array(repeating: Point(location: .zero, width: 0.0), count: 4) + private var pointPtr = 0 + + private var smoothPoints: [Point] = [] + private var activeSmoothPoints: [Point] = [] + + private var segments: [Segment] = [] + private var activeSegments: [Segment] = [] + + private var previousActiveRect: CGRect? + + private var previousRenderLineWidth: CGFloat? + + private func addPoint(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) -> (Bool, CGRect)? { + let filterDistance: CGFloat = 10.0 / zoomScale + + var velocity = point.velocity + if velocity.isZero { + velocity = 1000.0 + } + + var renderLineWidth = max(self.renderMinLineWidth, min(self.renderLineWidth - (velocity / 200.0), self.renderLineWidth)) + if let previousRenderLineWidth = self.previousRenderLineWidth { + renderLineWidth = renderLineWidth * 0.3 + previousRenderLineWidth * 0.7 + } + self.previousRenderLineWidth = renderLineWidth + + var resetActiveRect = false + var finalizedRect: CGRect? + if self.pointPtr == 0 { + self.points[0] = Point(location: point.location, width: renderLineWidth) + self.pointPtr += 1 + } else { + let previousPoint = self.points[self.pointPtr - 1].location + guard previousPoint.distance(to: point.location) > filterDistance else { + return nil + } + + if self.pointPtr >= 4 { + self.points[3] = Point( + location: self.points[2].location.point(to: point.location, t: 0.5), + width: self.points[2].width + ) + if var smoothPoints = self.currentSmoothPoints(3) { + if let previousSmoothPoint = self.smoothPoints.last { + smoothPoints.insert(previousSmoothPoint, at: 0) + } + let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints) + self.smoothPoints.append(contentsOf: smoothPoints) + self.segments.append(contentsOf: segments) + finalizedRect = rect + + self.activeSmoothPoints.removeAll() + self.activeSegments.removeAll() + + resetActiveRect = true + } + + self.points[0] = self.points[3] + self.pointPtr = 1 + } + + let point = Point(location: point.location, width: renderLineWidth) + self.points[self.pointPtr] = point + self.pointPtr += 1 + } + + guard let smoothPoints = self.currentSmoothPoints(self.pointPtr - 1) else { + if let finalizedRect { + return (resetActiveRect, finalizedRect) + } else { + return nil + } + } + + let (segments, rect) = self.segments(fromSmoothPoints: smoothPoints) + self.activeSmoothPoints = smoothPoints + self.activeSegments = segments + + var combinedRect: CGRect? + if let finalizedRect, let rect { + combinedRect = finalizedRect.union(rect) + } else { + combinedRect = rect ?? finalizedRect + } + if let combinedRect { + return (resetActiveRect, combinedRect) + } else { + return nil + } + } + + private func currentSmoothPoints(_ ctr: Int) -> [Point]? { + switch ctr { + case 0: + return [self.points[0]] + case 1: + return self.smoothPoints(.line(self.points[0], self.points[1])) + case 2: + return self.smoothPoints(.quad(self.points[0], self.points[1], self.points[2])) + case 3: + return self.smoothPoints(.cubic(self.points[0], self.points[1], self.points[2], self.points[3])) + default: + return nil + } + } + + private enum SmootherInput { + case line(Point, Point) + case quad(Point, Point, Point) + case cubic(Point, Point, Point, Point) + + var start: Point { + switch self { + case let .line(start, _), let .quad(start, _, _), let .cubic(start, _, _, _): + return start + } + } + + var end: Point { + switch self { + case let .line(_, end), let .quad(_, _, end), let .cubic(_, _, _, end): + return end + } + } + + var distance: CGFloat { + return self.start.location.distance(to: self.end.location) + } + } + private func smoothPoints(_ input: SmootherInput) -> [Point] { + let segmentDistance: CGFloat = 6.0 + let distance = input.distance + let numberOfSegments = min(48, max(floor(distance / segmentDistance), 24)) + + let step = 1.0 / numberOfSegments + + var smoothPoints: [Point] = [] + for t in stride(from: 0, to: 1, by: step) { + let point: Point + switch input { + case let .line(start, end): + point = Point( + location: start.location.linearBezierPoint(to: end.location, t: t), + width: CGPoint(x: start.width, y: 0.0).linearBezierPoint(to: CGPoint(x: end.width, y: 0.0), t: t).x + ) + case let .quad(start, control, end): + let location = start.location.quadBezierPoint(to: end.location, controlPoint: control.location, t: t) + let width = CGPoint(x: start.width, y: 0.0).quadBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint: CGPoint(x: (start.width + end.width) / 2.0, y: 0.0), t: t).x + point = Point( + location: location, + width: width + ) + case let .cubic(start, control1, control2, end): + let location = start.location.cubicBezierPoint(to: end.location, controlPoint1: control1.location, controlPoint2: control2.location, t: t) + let width = CGPoint(x: start.width, y: 0.0).cubicBezierPoint(to: CGPoint(x: end.width, y: 0.0), controlPoint1: CGPoint(x: (start.width + control1.width) / 2.0, y: 0.0), controlPoint2: CGPoint(x: (control2.width + end.width) / 2.0, y: 0.0), t: t).x + point = Point( + location: location, + width: width + ) + } + smoothPoints.append(point) + } + smoothPoints.append(input.end) + return smoothPoints + } + + fileprivate func boundingRect(from: Int, to: Int) -> CGRect { + var minX: CGFloat = .greatestFiniteMagnitude + var minY: CGFloat = .greatestFiniteMagnitude + var maxX: CGFloat = 0.0 + var maxY: CGFloat = 0.0 + + for i in from ..< to { + let segment = self.segments[i] + + if segment.rect.minX < minX { + minX = segment.rect.minX + } + if segment.rect.maxX > maxX { + maxX = segment.rect.maxX + } + if segment.rect.minY < minY { + minY = segment.rect.minY + } + if segment.rect.maxY > maxY { + maxY = segment.rect.maxY + } + } + + return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + } + + private func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect?) { + var segments: [Segment] = [] + var updateRect = CGRect.null + for i in 1 ..< smoothPoints.count { + let previousPoint = smoothPoints[i - 1].location + let previousWidth = smoothPoints[i - 1].width + let currentPoint = smoothPoints[i].location + let currentWidth = smoothPoints[i].width + let direction = CGPoint( + x: currentPoint.x - previousPoint.x, + y: currentPoint.y - previousPoint.y + ) + + guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else { + continue + } + + var perpendicular = CGPoint(x: -direction.y, y: direction.x) + let length = perpendicular.length + if length > 0.0 { + perpendicular = CGPoint( + x: perpendicular.x / length, + y: perpendicular.y / length + ) + } + + let a = CGPoint( + x: previousPoint.x + perpendicular.x * previousWidth / 2.0, + y: previousPoint.y + perpendicular.y * previousWidth / 2.0 + ) + let b = CGPoint( + x: previousPoint.x - perpendicular.x * previousWidth / 2.0, + y: previousPoint.y - perpendicular.y * previousWidth / 2.0 + ) + let c = CGPoint( + x: currentPoint.x + perpendicular.x * currentWidth / 2.0, + y: currentPoint.y + perpendicular.y * currentWidth / 2.0 + ) + let d = CGPoint( + x: currentPoint.x - perpendicular.x * currentWidth / 2.0, + y: currentPoint.y - perpendicular.y * currentWidth / 2.0 + ) + + let abCenter = CGPoint( + x: (a.x + b.x) / 2.0, + y: (a.y + b.y) / 2.0 + ) + let abRadius = CGPoint( + x: abCenter.x - b.x, + y: abCenter.y - b.y + ) + let ab = CGPoint( + x: abCenter.x - abRadius.y, + y: abCenter.y + abRadius.x + ) + + let cdCenter = CGPoint( + x: (c.x + d.x) / 2.0, + y: (c.y + d.y) / 2.0 + ) + let cdRadius = CGPoint( + x: cdCenter.x - c.x, + y: cdCenter.y - c.y + ) + let cd = CGPoint( + x: cdCenter.x - cdRadius.y, + y: cdCenter.y + cdRadius.x + ) + + let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x) + let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y) + let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x) + let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y) + + let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + updateRect = updateRect.union(segmentRect) + + let segment = Segment(a: a, b: b, c: c, d: d, radius1: previousWidth / 2.0, radius2: currentWidth / 2.0, rect: segmentRect) + segments.append(segment) + } + return (segments, !updateRect.isNull ? updateRect : nil) + } + + private var segmentPaths: [Int: CGPath] = [:] + + private func pathForSegment(_ segment: Segment) -> CGPath { + let path = CGMutablePath() + path.move(to: segment.b) + + let abStartAngle = atan2( + segment.b.y - segment.a.y, + segment.b.x - segment.a.x + ) + path.addArc( + center: CGPoint( + x: (segment.a.x + segment.b.x) / 2, + y: (segment.a.y + segment.b.y) / 2 + ), + radius: segment.radius1, + startAngle: abStartAngle, + endAngle: abStartAngle + .pi, + clockwise: true + ) + path.addLine(to: segment.c) + + let cdStartAngle = atan2( + segment.c.y - segment.d.y, + segment.c.x - segment.d.x + ) + path.addArc( + center: CGPoint( + x: (segment.c.x + segment.d.x) / 2, + y: (segment.c.y + segment.d.y) / 2 + ), + radius: segment.radius2, + startAngle: cdStartAngle, + endAngle: cdStartAngle + .pi, + clockwise: true + ) + path.closeSubpath() + return path + } + + private func drawSegments(in context: CGContext, from: Int, to: Int) { + context.setFillColor(self.renderColor.cgColor) + + for i in from ..< to { + let segment = self.segments[i] + + var segmentPath: CGPath + if let current = self.segmentPaths[i] { + segmentPath = current + } else { + let path = self.pathForSegment(segment) + self.segmentPaths[i] = path + segmentPath = path + } + + context.addPath(segmentPath) + context.fillPath() + } + } + + private func drawActiveSegments(in context: CGContext) { + context.setFillColor(self.renderColor.cgColor) + + for segment in self.activeSegments { + let path = self.pathForSegment(segment) + context.addPath(path) + context.fillPath() + } + } +} diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift new file mode 100644 index 00000000000..edc851263dd --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -0,0 +1,2996 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import AppBundle +import PresentationDataUtils +import LegacyComponents +import ComponentDisplayAdapters +import LottieAnimationComponent +import ViewControllerComponent +import ContextUI +import ChatEntityKeyboardInputNode +import EntityKeyboard +import TelegramUIPreferences +import FastBlur + +enum DrawingToolState: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case type + case brushState + case eraserState + } + + enum Key: Int32, RawRepresentable, CaseIterable, Codable { + case pen = 0 + case arrow = 1 + case marker = 2 + case neon = 3 + case blur = 4 + case eraser = 5 + } + + struct BrushState: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case color + case size + } + + let color: DrawingColor + let size: CGFloat + + init(color: DrawingColor, size: CGFloat) { + self.color = color + self.size = size + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.size = try container.decode(CGFloat.self, forKey: .size) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.color, forKey: .color) + try container.encode(self.size, forKey: .size) + } + + func withUpdatedColor(_ color: DrawingColor) -> BrushState { + return BrushState(color: color, size: self.size) + } + + func withUpdatedSize(_ size: CGFloat) -> BrushState { + return BrushState(color: self.color, size: size) + } + } + + struct EraserState: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case size + } + + let size: CGFloat + + init(size: CGFloat) { + self.size = size + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.size = try container.decode(CGFloat.self, forKey: .size) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.size, forKey: .size) + } + + func withUpdatedSize(_ size: CGFloat) -> EraserState { + return EraserState(size: size) + } + } + + case pen(BrushState) + case arrow(BrushState) + case marker(BrushState) + case neon(BrushState) + case blur(EraserState) + case eraser(EraserState) + + func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { + switch self { + case let .pen(state): + return .pen(state.withUpdatedColor(color)) + case let .arrow(state): + return .arrow(state.withUpdatedColor(color)) + case let .marker(state): + return .marker(state.withUpdatedColor(color)) + case let .neon(state): + return .neon(state.withUpdatedColor(color)) + case .blur, .eraser: + return self + } + } + + func withUpdatedSize(_ size: CGFloat) -> DrawingToolState { + switch self { + case let .pen(state): + return .pen(state.withUpdatedSize(size)) + case let .arrow(state): + return .arrow(state.withUpdatedSize(size)) + case let .marker(state): + return .marker(state.withUpdatedSize(size)) + case let .neon(state): + return .neon(state.withUpdatedSize(size)) + case let .blur(state): + return .blur(state.withUpdatedSize(size)) + case let .eraser(state): + return .eraser(state.withUpdatedSize(size)) + } + } + + var color: DrawingColor? { + switch self { + case let .pen(state), let .arrow(state), let .marker(state), let .neon(state): + return state.color + default: + return nil + } + } + + var size: CGFloat? { + switch self { + case let .pen(state), let .arrow(state), let .marker(state), let .neon(state): + return state.size + case let .blur(state), let .eraser(state): + return state.size + } + } + + var key: DrawingToolState.Key { + switch self { + case .pen: + return .pen + case .arrow: + return .arrow + case .marker: + return .marker + case .neon: + return .neon + case .blur: + return .blur + case .eraser: + return .eraser + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let typeValue = try container.decode(Int32.self, forKey: .type) + if let type = DrawingToolState.Key(rawValue: typeValue) { + switch type { + case .pen: + self = .pen(try container.decode(BrushState.self, forKey: .brushState)) + case .arrow: + self = .arrow(try container.decode(BrushState.self, forKey: .brushState)) + case .marker: + self = .marker(try container.decode(BrushState.self, forKey: .brushState)) + case .neon: + self = .neon(try container.decode(BrushState.self, forKey: .brushState)) + case .blur: + self = .blur(try container.decode(EraserState.self, forKey: .eraserState)) + case .eraser: + self = .eraser(try container.decode(EraserState.self, forKey: .eraserState)) + } + } else { + self = .pen(BrushState(color: DrawingColor(rgb: 0x000000), size: 0.5)) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .pen(state): + try container.encode(DrawingToolState.Key.pen.rawValue, forKey: .type) + try container.encode(state, forKey: .brushState) + case let .arrow(state): + try container.encode(DrawingToolState.Key.arrow.rawValue, forKey: .type) + try container.encode(state, forKey: .brushState) + case let .marker(state): + try container.encode(DrawingToolState.Key.marker.rawValue, forKey: .type) + try container.encode(state, forKey: .brushState) + case let .neon(state): + try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type) + try container.encode(state, forKey: .brushState) + case let .blur(state): + try container.encode(DrawingToolState.Key.blur.rawValue, forKey: .type) + try container.encode(state, forKey: .eraserState) + case let .eraser(state): + try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type) + try container.encode(state, forKey: .eraserState) + } + } +} + +struct DrawingState: Equatable { + let selectedTool: DrawingToolState.Key + let tools: [DrawingToolState] + + var currentToolState: DrawingToolState { + return self.toolState(for: self.selectedTool) + } + + func toolState(for key: DrawingToolState.Key) -> DrawingToolState { + for tool in self.tools { + if tool.key == key { + return tool + } + } + return .eraser(DrawingToolState.EraserState(size: 0.5)) + } + + func withUpdatedSelectedTool(_ selectedTool: DrawingToolState.Key) -> DrawingState { + return DrawingState( + selectedTool: selectedTool, + tools: self.tools + ) + } + + func withUpdatedTools(_ tools: [DrawingToolState]) -> DrawingState { + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + func withUpdatedColor(_ color: DrawingColor) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedColor(color) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + func withUpdatedSize(_ size: CGFloat) -> DrawingState { + var tools = self.tools + if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) { + let updated = tools[index].withUpdatedSize(size) + tools.remove(at: index) + tools.insert(updated, at: index) + } + + return DrawingState( + selectedTool: self.selectedTool, + tools: tools + ) + } + + static var initial: DrawingState { + return DrawingState( + selectedTool: .pen, + tools: [ + .pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff453a), size: 0.23)), + .arrow(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xff8a00), size: 0.23)), + .marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xffd60a), size: 0.75)), + .neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34c759), size: 0.4)), + .blur(DrawingToolState.EraserState(size: 0.5)), + .eraser(DrawingToolState.EraserState(size: 0.5)) + ] + ) + } + + func forVideo() -> DrawingState { + return DrawingState( + selectedTool: self.selectedTool, + tools: self.tools.filter { tool in + if case .blur = tool { + return false + } else { + return true + } + } + ) + } +} + +final class DrawingSettings: Codable, Equatable { + let tools: [DrawingToolState] + + init(tools: [DrawingToolState]) { + self.tools = tools + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let data = try container.decodeIfPresent(Data.self, forKey: "tools"), let tools = try? JSONDecoder().decode([DrawingToolState].self, from: data) { + self.tools = tools + } else { + self.tools = DrawingState.initial.tools + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + if let data = try? JSONEncoder().encode(self.tools) { + try container.encode(data, forKey: "tools") + } + } + + static func ==(lhs: DrawingSettings, rhs: DrawingSettings) -> Bool { + return lhs.tools == rhs.tools + } +} + +private final class ReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + private let contentArea: CGRect + private let customPosition: CGPoint + + init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) { + self.sourceView = sourceView + self.contentArea = contentArea + self.customPosition = customPosition + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: customPosition) + } +} + +private final class BlurredGradientComponent: Component { + enum Position { + case top + case bottom + } + + let position: Position + let tag: AnyObject? + + public init( + position: Position, + tag: AnyObject? + ) { + self.position = position + self.tag = tag + } + + public static func ==(lhs: BlurredGradientComponent, rhs: BlurredGradientComponent) -> Bool { + if lhs.position != rhs.position { + return false + } + return true + } + + public final class View: BlurredBackgroundView, ComponentTaggedView { + private var component: BlurredGradientComponent? + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private var gradientMask = UIImageView() + private var gradientForeground = SimpleGradientLayer() + + public func update(component: BlurredGradientComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + self.isUserInteractionEnabled = false + + self.updateColor(color: UIColor(rgb: 0x000000, alpha: component.position == .top ? 0.15 : 0.25), transition: transition.containedViewLayoutTransition) + + if self.mask == nil { + self.mask = self.gradientMask + self.gradientMask.image = generateGradientImage( + size: CGSize(width: 1.0, height: availableSize.height), + colors: [UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 1.0), UIColor(rgb: 0xffffff, alpha: 0.0)], + locations: component.position == .top ? [0.0, 0.5, 1.0] : [1.0, 0.5, 0.0], + direction: .vertical + ) + + self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.35).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor] + self.gradientForeground.startPoint = CGPoint(x: 0.5, y: component.position == .top ? 0.0 : 1.0) + self.gradientForeground.endPoint = CGPoint(x: 0.5, y: component.position == .top ? 1.0 : 0.0) + + self.layer.addSublayer(self.gradientForeground) + } + + transition.setFrame(view: self.gradientMask, frame: CGRect(origin: .zero, size: availableSize)) + transition.setFrame(layer: self.gradientForeground, frame: CGRect(origin: .zero, size: availableSize)) + + self.update(size: availableSize, transition: transition.containedViewLayoutTransition) + + return availableSize + } + } + + public func makeView() -> View { + return View(color: nil, enableBlur: true) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + + +enum DrawingScreenTransition { + case animateIn + case animateOut +} + +private let topGradientTag = GenericComponentViewTag() +private let bottomGradientTag = GenericComponentViewTag() +private let undoButtonTag = GenericComponentViewTag() +private let redoButtonTag = GenericComponentViewTag() +private let clearAllButtonTag = GenericComponentViewTag() +private let colorButtonTag = GenericComponentViewTag() +private let addButtonTag = GenericComponentViewTag() +private let toolsTag = GenericComponentViewTag() +private let modeTag = GenericComponentViewTag() +private let flipButtonTag = GenericComponentViewTag() +private let fillButtonTag = GenericComponentViewTag() +private let zoomOutButtonTag = GenericComponentViewTag() +private let textSettingsTag = GenericComponentViewTag() +private let sizeSliderTag = GenericComponentViewTag() +private let fontTag = GenericComponentViewTag() +private let color1Tag = GenericComponentViewTag() +private let color2Tag = GenericComponentViewTag() +private let color3Tag = GenericComponentViewTag() +private let color4Tag = GenericComponentViewTag() +private let color5Tag = GenericComponentViewTag() +private let color6Tag = GenericComponentViewTag() +private let color7Tag = GenericComponentViewTag() +private let color8Tag = GenericComponentViewTag() +private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag, color6Tag, color7Tag, color8Tag] +private let doneButtonTag = GenericComponentViewTag() + +private final class DrawingScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let isVideo: Bool + let isAvatar: Bool + let present: (ViewController) -> Void + let updateState: ActionSlot + let updateColor: ActionSlot + let performAction: ActionSlot + let updateToolState: ActionSlot + let updateSelectedEntity: ActionSlot + let insertEntity: ActionSlot + let deselectEntity: ActionSlot + let updateEntitiesPlayback: ActionSlot + let previewBrushSize: ActionSlot + let dismissEyedropper: ActionSlot + let requestPresentColorPicker: ActionSlot + let toggleWithEraser: ActionSlot + let toggleWithPreviousTool: ActionSlot + let apply: ActionSlot + let dismiss: ActionSlot + + let presentColorPicker: (DrawingColor) -> Void + let presentFastColorPicker: (UIView) -> Void + let updateFastColorPickerPan: (CGPoint) -> Void + let dismissFastColorPicker: () -> Void + let presentFontPicker: (UIView) -> Void + + init( + context: AccountContext, + isVideo: Bool, + isAvatar: Bool, + present: @escaping (ViewController) -> Void, + updateState: ActionSlot, + updateColor: ActionSlot, + performAction: ActionSlot, + updateToolState: ActionSlot, + updateSelectedEntity: ActionSlot, + insertEntity: ActionSlot, + deselectEntity: ActionSlot, + updateEntitiesPlayback: ActionSlot, + previewBrushSize: ActionSlot, + dismissEyedropper: ActionSlot, + requestPresentColorPicker: ActionSlot, + toggleWithEraser: ActionSlot, + toggleWithPreviousTool: ActionSlot, + apply: ActionSlot, + dismiss: ActionSlot, + presentColorPicker: @escaping (DrawingColor) -> Void, + presentFastColorPicker: @escaping (UIView) -> Void, + updateFastColorPickerPan: @escaping (CGPoint) -> Void, + dismissFastColorPicker: @escaping () -> Void, + presentFontPicker: @escaping (UIView) -> Void + ) { + self.context = context + self.isVideo = isVideo + self.isAvatar = isAvatar + self.present = present + self.updateState = updateState + self.updateColor = updateColor + self.performAction = performAction + self.updateToolState = updateToolState + self.updateSelectedEntity = updateSelectedEntity + self.insertEntity = insertEntity + self.deselectEntity = deselectEntity + self.updateEntitiesPlayback = updateEntitiesPlayback + self.previewBrushSize = previewBrushSize + self.dismissEyedropper = dismissEyedropper + self.requestPresentColorPicker = requestPresentColorPicker + self.toggleWithEraser = toggleWithEraser + self.toggleWithPreviousTool = toggleWithPreviousTool + self.apply = apply + self.dismiss = dismiss + self.presentColorPicker = presentColorPicker + self.presentFastColorPicker = presentFastColorPicker + self.updateFastColorPickerPan = updateFastColorPickerPan + self.dismissFastColorPicker = dismissFastColorPicker + self.presentFontPicker = presentFontPicker + } + + static func ==(lhs: DrawingScreenComponent, rhs: DrawingScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.isAvatar != rhs.isAvatar { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case undo + case redo + case done + case add + case fill + case stroke + case flip + case zoomOut + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .undo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)! + case .redo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)! + case .done: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! + case .add: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Add"), color: .white)! + case .fill: + image = UIImage(bundleImageName: "Media Editor/Fill")! + case .stroke: + image = UIImage(bundleImageName: "Media Editor/Stroke")! + case .flip: + image = UIImage(bundleImageName: "Media Editor/Flip")! + case .zoomOut: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ZoomOut"), color: .white)! + } + cachedImages[key] = image + return image + } + } + + enum Mode { + case drawing + case sticker + case text + } + + private let context: AccountContext + private let updateToolState: ActionSlot + private let insertEntity: ActionSlot + private let deselectEntity: ActionSlot + private let updateEntitiesPlayback: ActionSlot + private let dismissEyedropper: ActionSlot + private let toggleWithEraser: ActionSlot + private let toggleWithPreviousTool: ActionSlot + private let present: (ViewController) -> Void + + var currentMode: Mode + var drawingState: DrawingState + var drawingViewState: DrawingView.NavigationState + var currentColor: DrawingColor + var selectedEntity: DrawingEntity? + + var lastSize: CGFloat = 0.5 + + private let stickerPickerInputData = Promise() + + init(context: AccountContext, updateToolState: ActionSlot, insertEntity: ActionSlot, deselectEntity: ActionSlot, updateEntitiesPlayback: ActionSlot, dismissEyedropper: ActionSlot, toggleWithEraser: ActionSlot, toggleWithPreviousTool: ActionSlot, present: @escaping (ViewController) -> Void) { + self.context = context + self.updateToolState = updateToolState + self.insertEntity = insertEntity + self.deselectEntity = deselectEntity + self.updateEntitiesPlayback = updateEntitiesPlayback + self.dismissEyedropper = dismissEyedropper + self.toggleWithEraser = toggleWithEraser + self.toggleWithPreviousTool = toggleWithPreviousTool + self.present = present + + self.currentMode = .drawing + self.drawingState = .initial + self.drawingViewState = DrawingView.NavigationState(canUndo: false, canRedo: false, canClear: false, canZoomOut: false, isDrawing: false) + self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff) + + self.updateToolState.invoke(self.drawingState.currentToolState) + + let stickerPickerInputData = self.stickerPickerInputData + Queue.concurrentDefaultQueue().after(0.5, { + let emojiItems = EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + isStandalone: false, + isStatusSelection: false, + isReactionSelection: false, + isEmojiSelection: true, + topReactionItems: [], + areUnicodeEmojiEnabled: true, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + hasSearch: false, + forceHasPremium: true + ) + + let stickerItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], + stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], + chatPeerId: context.account.peerId, + hasSearch: false, + hasTrending: true, + forceHasPremium: true + ) + + let maskItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], + stickerOrderedItemListCollectionIds: [], + chatPeerId: context.account.peerId, + hasSearch: false, + hasTrending: false, + forceHasPremium: true + ) + + let signal = combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems, + maskItems + ) |> map { emoji, stickers, masks -> StickerPickerInputData in + return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks) + } + + stickerPickerInputData.set(signal) + }) + + super.init() + + self.loadToolState() + + self.toggleWithEraser.connect { [weak self] _ in + if let strongSelf = self { + if strongSelf.drawingState.selectedTool == .eraser { + strongSelf.updateSelectedTool(strongSelf.nextToEraserTool) + } else { + strongSelf.updateSelectedTool(.eraser) + } + } + } + + self.toggleWithPreviousTool.connect { [weak self] _ in + if let strongSelf = self { + strongSelf.updateSelectedTool(strongSelf.previousTool) + } + } + } + + func loadToolState() { + let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.drawingSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + guard let strongSelf = self else { + return + } + if let drawingSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.drawingSettings]?.get(DrawingSettings.self) { + strongSelf.drawingState = strongSelf.drawingState.withUpdatedTools(drawingSettings.tools) + strongSelf.currentColor = strongSelf.drawingState.currentToolState.color ?? strongSelf.currentColor + strongSelf.updated(transition: .immediate) + strongSelf.updateToolState.invoke(strongSelf.drawingState.currentToolState) + } + }) + } + + func saveToolState() { + let tools = self.drawingState.tools + let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.drawingSettings, { _ in + return PreferencesEntry(DrawingSettings(tools: tools)) + }) + }).start() + } + + private var currentToolState: DrawingToolState { + return self.drawingState.toolState(for: self.drawingState.selectedTool) + } + + func updateColor(_ color: DrawingColor, animated: Bool = false) { + self.currentColor = color + if let selectedEntity = self.selectedEntity { + selectedEntity.color = color + selectedEntity.currentEntityView?.update() + } else { + self.drawingState = self.drawingState.withUpdatedColor(color) + self.updateToolState.invoke(self.drawingState.currentToolState) + } + self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate) + } + + var previousTool: DrawingToolState.Key = .eraser + var nextToEraserTool: DrawingToolState.Key = .pen + + func updateSelectedTool(_ tool: DrawingToolState.Key, update: Bool = true) { + if self.selectedEntity != nil { + self.skipSelectedEntityUpdate = true + self.updateCurrentMode(.drawing, update: false) + self.skipSelectedEntityUpdate = false + } + + if tool != self.drawingState.selectedTool { + if self.drawingState.selectedTool == .eraser { + self.nextToEraserTool = tool + } else if tool == .eraser { + self.nextToEraserTool = self.drawingState.selectedTool + } + self.previousTool = self.drawingState.selectedTool + } + + self.drawingState = self.drawingState.withUpdatedSelectedTool(tool) + self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor + self.updateToolState.invoke(self.drawingState.currentToolState) + if update { + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func updateBrushSize(_ size: CGFloat) { + if let selectedEntity = self.selectedEntity { + if let textEntity = selectedEntity as? DrawingTextEntity { + textEntity.fontSize = size + } else { + selectedEntity.lineWidth = size + } + selectedEntity.currentEntityView?.update() + } else { + self.drawingState = self.drawingState.withUpdatedSize(size) + self.updateToolState.invoke(self.drawingState.currentToolState) + } + self.updated(transition: .immediate) + } + + func updateDrawingState(_ state: DrawingView.NavigationState) { + self.drawingViewState = state + self.updated(transition: .easeInOut(duration: 0.2)) + } + + var skipSelectedEntityUpdate = false + func updateSelectedEntity(_ entity: DrawingEntity?) { + self.dismissEyedropper.invoke(Void()) + + self.selectedEntity = entity + if let entity = entity { + if !entity.color.isClear { + self.currentColor = entity.color + } + if entity is DrawingStickerEntity { + self.currentMode = .sticker + } else if entity is DrawingTextEntity { + self.currentMode = .text + } else { + self.currentMode = .drawing + } + } else { + self.currentMode = .drawing + self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor + } + if !self.skipSelectedEntityUpdate { + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func presentShapePicker(_ sourceView: UIView) { + let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings + + let items: [ContextMenuItem] = [ + .action( + ContextMenuActionItem( + text: strings.Paint_Rectangle, + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: strings.Paint_Ellipse, + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: strings.Paint_Bubble, + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingBubbleEntity(drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: strings.Paint_Star, + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.currentColor, lineWidth: 0.15)) + } + } + ) + ), + .action( + ContextMenuActionItem( + text: strings.Paint_Arrow, + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)}, + action: { [weak self] f in + f.dismissWithResult(.default) + if let strongSelf = self { + strongSelf.insertEntity.invoke(DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.currentColor, lineWidth: 0.3)) + } + } + ) + ) + ] + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(x: 7.0, y: 3.0))), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController) + } + + func updateCurrentMode(_ mode: Mode, update: Bool = true) { + self.currentMode = mode + if let selectedEntity = self.selectedEntity { + if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity { + self.deselectEntity.invoke(Void()) + } + } + if update { + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func addTextEntity() { + let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) + self.insertEntity.invoke(textEntity) + } + + func presentStickerPicker() { + self.currentMode = .sticker + + self.updateEntitiesPlayback.invoke(false) + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) + controller.completion = { [weak self] file in + self?.updateEntitiesPlayback.invoke(true) + + if let file = file { + let stickerEntity = DrawingStickerEntity(file: file) + self?.insertEntity.invoke(stickerEntity) + } else { + self?.updateCurrentMode(.drawing) + } + } + self.present(controller) + self.updated(transition: .easeInOut(duration: 0.2)) + } + } + + func makeState() -> State { + return State(context: self.context, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, present: self.present) + } + + static var body: Body { + let topGradient = Child(BlurredGradientComponent.self) + let bottomGradient = Child(BlurredGradientComponent.self) + + let undoButton = Child(Button.self) + + let redoButton = Child(Button.self) + let clearAllButton = Child(Button.self) + + let zoomOutButton = Child(Button.self) + + let tools = Child(ToolsComponent.self) + let modeAndSize = Child(ModeAndSizeComponent.self) + + let colorButton = Child(ColorSwatchComponent.self) + + let textSettings = Child(TextSettingsComponent.self) + + let swatch1Button = Child(ColorSwatchComponent.self) + let swatch2Button = Child(ColorSwatchComponent.self) + let swatch3Button = Child(ColorSwatchComponent.self) + let swatch4Button = Child(ColorSwatchComponent.self) + let swatch5Button = Child(ColorSwatchComponent.self) + let swatch6Button = Child(ColorSwatchComponent.self) + let swatch7Button = Child(ColorSwatchComponent.self) + let swatch8Button = Child(ColorSwatchComponent.self) + + let addButton = Child(Button.self) + + let flipButton = Child(Button.self) + let fillButton = Child(Button.self) + + let backButton = Child(Button.self) + let doneButton = Child(Button.self) + + let textSize = Child(TextSizeSliderComponent.self) + let textCancelButton = Child(Button.self) + let textDoneButton = Child(Button.self) + + let presetColors: [DrawingColor] = [ + DrawingColor(rgb: 0xff453a), + DrawingColor(rgb: 0xff8a00), + DrawingColor(rgb: 0xffd60a), + DrawingColor(rgb: 0x34c759), + DrawingColor(rgb: 0x63e6e2), + DrawingColor(rgb: 0x0a84ff), + DrawingColor(rgb: 0xbf5af2), + DrawingColor(rgb: 0xffffff) + ] + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let state = context.state + let controller = environment.controller + + let strings = environment.strings + + let previewBrushSize = component.previewBrushSize + let performAction = component.performAction + let dismissEyedropper = component.dismissEyedropper + + let apply = component.apply + let dismiss = component.dismiss + + let presentColorPicker = component.presentColorPicker + let presentFastColorPicker = component.presentFastColorPicker + let updateFastColorPickerPan = component.updateFastColorPickerPan + let dismissFastColorPicker = component.dismissFastColorPicker + let presentFontPicker = component.presentFontPicker + + component.updateState.connect { [weak state] updatedState in + state?.updateDrawingState(updatedState) + } + component.updateColor.connect { [weak state] color in + if let state = state { + if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity { + state.updateSelectedTool(.pen, update: false) + state.updateColor(color, animated: true) + } else { + state.updateColor(color) + } + + } + } + component.updateSelectedEntity.connect { [weak state] entity in + state?.updateSelectedEntity(entity) + } + component.requestPresentColorPicker.connect { [weak state] _ in + if let state = state { + presentColorPicker(state.currentColor) + } + } + + let topInset = environment.safeInsets.top + 31.0 + let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0 + + var leftEdge: CGFloat = environment.safeInsets.left + var rightEdge: CGFloat = context.availableSize.width - environment.safeInsets.right + var availableWidth = context.availableSize.width + if case .regular = environment.metrics.widthClass { + availableWidth = 430.0 + leftEdge = floorToScreenPixels((context.availableSize.width - availableWidth) / 2.0) + rightEdge = floorToScreenPixels((context.availableSize.width - availableWidth) / 2.0) + availableWidth + } + + let topGradient = topGradient.update( + component: BlurredGradientComponent( + position: .top, + tag: topGradientTag + ), + availableSize: CGSize(width: context.availableSize.width, height: topInset + 10.0), + transition: .immediate + ) + context.add(topGradient + .position(CGPoint(x: context.availableSize.width / 2.0, y: topGradient.size.height / 2.0)) + ) + + let bottomGradient = bottomGradient.update( + component: BlurredGradientComponent( + position: .bottom, + tag: bottomGradientTag + + ), + availableSize: CGSize(width: context.availableSize.width, height: 155.0), + transition: .immediate + ) + context.add(bottomGradient + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomGradient.size.height / 2.0)) + ) + + if let textEntity = state.selectedEntity as? DrawingTextEntity { + let textSettings = textSettings.update( + component: TextSettingsComponent( + color: nil, + style: DrawingTextStyle(style: textEntity.style), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + isEmojiKeyboard: false, + tag: textSettingsTag, + fontTag: fontTag, + toggleStyle: { [weak state, weak textEntity] in + guard let textEntity = textEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + }, + toggleAlignment: { [weak state, weak textEntity] in + guard let textEntity = textEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + if let entityView = textEntity.currentEntityView { + entityView.update() + } + state?.updated(transition: .easeInOut(duration: 0.2)) + }, + presentFontPicker: { + if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: fontTag) { + presentFontPicker(buttonView) + } + }, + toggleKeyboard: nil + ), + availableSize: CGSize(width: availableWidth - 84.0, height: 44.0), + transition: context.transition + ) + context.add(textSettings + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - textSettings.size.height / 2.0 - 89.0)) + .appear(Transition.Appear({ _, view, transition in + if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate { + view.animateIn() + } + })) + .disappear(Transition.Disappear({ view, transition, completion in + if let view = view as? TextSettingsComponent.View, !transition.animation.isImmediate { + view.animateOut(completion: completion) + } else { + completion() + } + })) + ) + } + + + let rightButtonPosition = rightEdge - 24.0 + var offsetX: CGFloat = leftEdge + 24.0 + let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0 + + let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in + dismissEyedropper.invoke(Void()) + if let state = state { + if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity { + state.updateSelectedTool(.pen, update: false) + } + state.updateColor(color, animated: true) + } + } + + var currentColor: DrawingColor? = state.currentColor + if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity { + currentColor = nil + } + + var delay: Double = 0.0 + let swatch1Button = swatch1Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[0]), + color: presetColors[0], + tag: color1Tag, + action: { + applySwatchColor(presetColors[0]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch1Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch1Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch2Button = swatch2Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[1]), + color: presetColors[1], + tag: color2Tag, + action: { + applySwatchColor(presetColors[1]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch2Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch2Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.025) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.025) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch3Button = swatch3Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[2]), + color: presetColors[2], + tag: color3Tag, + action: { + applySwatchColor(presetColors[2]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch3Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch3Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.05) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.05) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch4Button = swatch4Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[3]), + color: presetColors[3], + tag: color4Tag, + action: { + applySwatchColor(presetColors[3]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch4Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch4Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.075) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.075) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch5Button = swatch5Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[4]), + color: presetColors[4], + tag: color5Tag, + action: { + applySwatchColor(presetColors[4]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch5Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch5Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.1) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.1) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + delay += 0.025 + + let swatch6Button = swatch6Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[5]), + color: presetColors[5], + tag: color6Tag, + action: { + applySwatchColor(presetColors[5]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch6Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch6Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.125) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.125) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch7Button = swatch7Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[6]), + color: presetColors[6], + tag: color7Tag, + action: { + applySwatchColor(presetColors[6]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch7Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.15) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.15) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + offsetX += delta + + let swatch8Button = swatch8Button.update( + component: ColorSwatchComponent( + type: .pallete(currentColor == presetColors[7]), + color: presetColors[7], + tag: color8Tag, + action: { + applySwatchColor(presetColors[7]) + } + ), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(swatch8Button + .position(CGPoint(x: offsetX, y: context.availableSize.height - environment.safeInsets.bottom - swatch7Button.size.height / 2.0 - 57.0)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0, delay: 0.175) + transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: 0.175) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + }) + ) + + if state.selectedEntity is DrawingStickerEntity || state.selectedEntity is DrawingTextEntity { + } else { + let tools = tools.update( + component: ToolsComponent( + state: component.isVideo ? state.drawingState.forVideo() : state.drawingState, + isFocused: false, + tag: toolsTag, + toolPressed: { [weak state] tool in + dismissEyedropper.invoke(Void()) + if let state = state { + state.updateSelectedTool(tool) + } + }, + toolResized: { [weak state] _, size in + dismissEyedropper.invoke(Void()) + state?.updateBrushSize(size) + if state?.selectedEntity == nil { + previewBrushSize.invoke(size) + } + }, + sizeReleased: { + previewBrushSize.invoke(nil) + } + ), + availableSize: CGSize(width: availableWidth - environment.safeInsets.left - environment.safeInsets.right, height: 120.0), + transition: context.transition + ) + context.add(tools + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - tools.size.height / 2.0 - 78.0)) + .appear(Transition.Appear({ _, view, transition in + if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { + view.animateIn(completion: {}) + } + })) + .disappear(Transition.Disappear({ view, transition, completion in + if let view = view as? ToolsComponent.View, !transition.animation.isImmediate { + view.animateOut(completion: completion) + } else { + completion() + } + })) + ) + } + + var hasTopButtons = false + if let entity = state.selectedEntity { + var isFilled: Bool? + if let entity = entity as? DrawingSimpleShapeEntity { + isFilled = entity.drawType == .fill + } else if let entity = entity as? DrawingBubbleEntity { + isFilled = entity.drawType == .fill + } else if let _ = entity as? DrawingVectorEntity { + isFilled = false + } + + var hasFlip = false + if state.selectedEntity is DrawingBubbleEntity || state.selectedEntity is DrawingStickerEntity { + hasFlip = true + } + + hasTopButtons = isFilled != nil || hasFlip + + if let isFilled = isFilled { + let fillButton = fillButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(isFilled ? .fill : .stroke)) + ), + action: { [weak state] in + guard let state = state else { + return + } + if let entity = state.selectedEntity as? DrawingSimpleShapeEntity { + if case .fill = entity.drawType { + entity.drawType = .stroke + } else { + entity.drawType = .fill + } + entity.currentEntityView?.update() + } else if let entity = state.selectedEntity as? DrawingBubbleEntity { + if case .fill = entity.drawType { + entity.drawType = .stroke + } else { + entity.drawType = .fill + } + entity.currentEntityView?.update() + } else if let entity = state.selectedEntity as? DrawingVectorEntity { + if case .oneSidedArrow = entity.type { + entity.type = .twoSidedArrow + } else if case .twoSidedArrow = entity.type { + entity.type = .line + } else { + entity.type = .oneSidedArrow + } + entity.currentEntityView?.update() + } + state.updated(transition: .easeInOut(duration: 0.2)) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(fillButton + .position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } + + if hasFlip { + let flipButton = flipButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.flip)) + ), + action: { [weak state] in + guard let state = state else { + return + } + if let entity = state.selectedEntity as? DrawingBubbleEntity { + var updatedTailPosition = entity.tailPosition + updatedTailPosition.x = 1.0 - updatedTailPosition.x + entity.tailPosition = updatedTailPosition + entity.currentEntityView?.update() + } else if let entity = state.selectedEntity as? DrawingStickerEntity { + entity.mirrored = !entity.mirrored + entity.currentEntityView?.update(animated: true) + } + state.updated(transition: .easeInOut(duration: 0.2)) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(flipButton + .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } + } + + var sizeSliderVisible = false + var isEditingText = false + var sizeValue: CGFloat? + if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView { + sizeSliderVisible = true + isEditingText = entityView.isEditing + sizeValue = textEntity.fontSize + } else { + if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) { + sizeSliderVisible = true + if state.selectedEntity == nil { + sizeValue = state.drawingState.currentToolState.size + } else if let entity = state.selectedEntity { + if let entity = entity as? DrawingSimpleShapeEntity { + sizeSliderVisible = entity.drawType == .stroke + } else if let entity = entity as? DrawingBubbleEntity { + sizeSliderVisible = entity.drawType == .stroke + } + sizeValue = entity.lineWidth + } + } + if state.drawingViewState.canZoomOut && !hasTopButtons { + let zoomOutButton = zoomOutButton.update( + component: Button( + content: AnyComponent( + ZoomOutButtonContent( + title: strings.Paint_ZoomOut, + image: state.image(.zoomOut) + ) + ), + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.zoomOut) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag), + availableSize: CGSize(width: 120.0, height: 33.0), + transition: .immediate + ) + context.add(zoomOutButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + } + if let sizeValue { + state.lastSize = sizeValue + } + if state.drawingViewState.isDrawing { + sizeSliderVisible = false + } + + let textSize = textSize.update( + component: TextSizeSliderComponent( + value: sizeValue ?? state.lastSize, + tag: sizeSliderTag, + updated: { [weak state] size in + if let state = state { + dismissEyedropper.invoke(Void()) + state.updateBrushSize(size) + if state.selectedEntity == nil { + previewBrushSize.invoke(size) + } + } + }, released: { + previewBrushSize.invoke(nil) + } + ), + availableSize: CGSize(width: 30.0, height: 240.0), + transition: context.transition + ) + context.add(textSize + .position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0)) + .opacity(sizeSliderVisible ? 1.0 : 0.0) + ) + + let undoButton = undoButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.undo)) + ), + isEnabled: state.drawingViewState.canUndo, + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.undo) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(undoButton + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset)) + .scale(isEditingText ? 0.01 : 1.0) + .opacity(isEditingText ? 0.0 : 1.0) + ) + + + let redoButton = redoButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.redo)) + ), + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.redo) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: context.transition + ) + context.add(redoButton + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset)) + .scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01) + .opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0) + ) + + let clearAllButton = clearAllButton.update( + component: Button( + content: AnyComponent( + Text(text: strings.Paint_Clear, font: Font.regular(17.0), color: .white) + ), + isEnabled: state.drawingViewState.canClear, + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.clear) + } + ).tagged(clearAllButtonTag), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(clearAllButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset)) + .scale(isEditingText ? 0.01 : 1.0) + .opacity(isEditingText ? 0.0 : 1.0) + ) + + let textCancelButton = textCancelButton.update( + component: Button( + content: AnyComponent( + Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white) + ), + action: { [weak state] in + if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { + entityView.endEditing(reset: true) + } + } + ), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(textCancelButton + .position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset)) + .scale(isEditingText ? 1.0 : 0.01) + .opacity(isEditingText ? 1.0 : 0.0) + ) + + let textDoneButton = textDoneButton.update( + component: Button( + content: AnyComponent( + Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white) + ), + action: { [weak state] in + if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView { + entityView.endEditing() + } + } + ), + availableSize: CGSize(width: 100.0, height: 30.0), + transition: context.transition + ) + context.add(textDoneButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset)) + .scale(isEditingText ? 1.0 : 0.01) + .opacity(isEditingText ? 1.0 : 0.0) + ) + + var color: DrawingColor? + if let entity = state.selectedEntity, presetColors.contains(entity.color) { + color = nil + } else if presetColors.contains(state.currentColor) { + color = nil + } else if state.selectedEntity is DrawingStickerEntity { + color = nil + } else if [.eraser, .blur].contains(state.drawingState.selectedTool) { + color = nil + } else { + color = state.currentColor + } + + let colorButton = colorButton.update( + component: ColorSwatchComponent( + type: .main, + color: color, + tag: colorButtonTag, + action: { [weak state] in + if let state = state { + presentColorPicker(state.currentColor) + } + }, + holdAction: { + if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: colorButtonTag) { + presentFastColorPicker(buttonView) + } + }, + pan: { point in + updateFastColorPickerPan(point) + }, + release: { + dismissFastColorPicker() + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: context.transition + ) + context.add(colorButton + .position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + + let modeRightInset: CGFloat = 57.0 + let addButton = addButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + BlurredRectangle( + color: UIColor(rgb: 0x888888, alpha: 0.3), + radius: 12.0 + ) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: state.image(.add)) + ) + ), + ])), + action: { [weak state] in + guard let controller = controller() as? DrawingScreen, let state = state else { + return + } + switch state.currentMode { + case .drawing: + dismissEyedropper.invoke(Void()) + if let buttonView = controller.node.componentHost.findTaggedView(tag: addButtonTag) as? Button.View { + state.presentShapePicker(buttonView) + } + case .sticker: + dismissEyedropper.invoke(Void()) + state.presentStickerPicker() + case .text: + dismissEyedropper.invoke(Void()) + state.addTextEntity() + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(addButtonTag), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: .immediate + ) + context.add(addButton + .position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + + let doneButton = doneButton.update( + component: Button( + content: AnyComponent( + Image(image: state.image(.done)) + ), + action: { [weak state] in + dismissEyedropper.invoke(Void()) + state?.saveToolState() + apply.invoke(Void()) + } + ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(doneButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + .appear(Transition.Appear { _, view, transition in + transition.animateScale(view: view, from: 0.1, to: 1.0) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + + transition.animatePosition(view: view, from: CGPoint(x: 12.0, y: 0.0), to: CGPoint(), additive: true) + }) + .disappear(Transition.Disappear { view, transition, completion in + transition.setScale(view: view, scale: 0.1) + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + completion() + }) + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true) + }) + ) + + let selectedIndex: Int + switch state.currentMode { + case .drawing: + selectedIndex = 0 + case .sticker: + selectedIndex = 1 + case .text: + selectedIndex = 2 + } + + var selectedSize: CGFloat = 0.0 + if let entity = state.selectedEntity { + selectedSize = entity.lineWidth + } else { + selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0 + } + + let modeAndSize = modeAndSize.update( + component: ModeAndSizeComponent( + values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text], + sizeValue: selectedSize, + isEditing: false, + isEnabled: true, + rightInset: modeRightInset - 57.0, + tag: modeTag, + selectedIndex: selectedIndex, + selectionChanged: { [weak state] index in + dismissEyedropper.invoke(Void()) + guard let state = state else { + return + } + switch index { + case 1: + state.presentStickerPicker() + case 2: + state.addTextEntity() + default: + state.updateCurrentMode(.drawing) + } + }, + sizeUpdated: { [weak state] size in + if let state = state { + dismissEyedropper.invoke(Void()) + state.updateBrushSize(size) + if state.selectedEntity == nil { + previewBrushSize.invoke(size) + } + } + }, + sizeReleased: { + previewBrushSize.invoke(nil) + } + ), + availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height), + transition: context.transition + ) + context.add(modeAndSize + .position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)) + ) + + var animatingOut = false + if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition { + animatingOut = true + } + + let backButton = backButton.update( + component: Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { [weak state] in + if let state = state { + dismissEyedropper.invoke(Void()) + state.saveToolState() + dismiss.invoke(Void()) + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 33.0, height: 33.0), + transition: .immediate + ) + context.add(backButton + .position(CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)) + ) + + return context.availableSize + } + } +} + +public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController { + fileprivate final class Node: ViewControllerTracingNode { + private weak var controller: DrawingScreen? + private let context: AccountContext + private let updateState: ActionSlot + private let updateColor: ActionSlot + private let performAction: ActionSlot + private let updateToolState: ActionSlot + private let updateSelectedEntity: ActionSlot + private let insertEntity: ActionSlot + private let deselectEntity: ActionSlot + private let updateEntitiesPlayback: ActionSlot + private let previewBrushSize: ActionSlot + private let dismissEyedropper: ActionSlot + + private let requestPresentColorPicker: ActionSlot + private let toggleWithEraser: ActionSlot + private let toggleWithPreviousTool: ActionSlot + + private let apply: ActionSlot + private let dismiss: ActionSlot + + fileprivate let componentHost: ComponentView + + private let textEditAccessoryView: UIInputView + private let textEditAccessoryHost: ComponentView + + private var presentationData: PresentationData + private let hapticFeedback = HapticFeedback() + private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)? + + private var _drawingView: DrawingView? + var drawingView: DrawingView { + if self._drawingView == nil, let controller = self.controller { + self._drawingView = DrawingView(size: controller.size) + self._drawingView?.shouldBegin = { [weak self] _ in + if let strongSelf = self { + if strongSelf._entitiesView?.hasSelection == true { + strongSelf._entitiesView?.selectEntity(nil) + return false + } + return true + } else { + return false + } + } + self._drawingView?.stateUpdated = { [weak self] state in + if let strongSelf = self { + strongSelf.updateState.invoke(state) + } + } + self._drawingView?.requestedColorPicker = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.colorPickerScreen { + strongSelf.dismissColorPicker() + } else { + strongSelf.requestPresentColorPicker.invoke(Void()) + } + } + } + self._drawingView?.requestedEraserToggle = { [weak self] in + if let strongSelf = self { + strongSelf.toggleWithEraser.invoke(Void()) + } + } + self._drawingView?.requestedToolsToggle = { [weak self] in + if let strongSelf = self { + strongSelf.toggleWithPreviousTool.invoke(Void()) + } + } + self.performAction.connect { [weak self] action in + if let strongSelf = self { + if action == .clear { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + + self?._drawingView?.performAction(action) + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf._drawingView?.performAction(action) + } + } + } + self.updateToolState.connect { [weak self] state in + if let strongSelf = self { + strongSelf._drawingView?.updateToolState(state) + } + } + self.previewBrushSize.connect { [weak self] size in + if let strongSelf = self { + strongSelf._drawingView?.setBrushSizePreview(size) + } + } + self.dismissEyedropper.connect { [weak self] in + if let strongSelf = self { + strongSelf.dismissCurrentEyedropper() + } + } + } + return self._drawingView! + } + + private weak var currentMenuController: ContextMenuController? + private var _entitiesView: DrawingEntitiesView? + var entitiesView: DrawingEntitiesView { + if self._entitiesView == nil, let controller = self.controller { + if let externalEntitiesView = controller.externalEntitiesView { + self._entitiesView = externalEntitiesView + } else { + self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size) + } + self._drawingView?.entitiesView = self._entitiesView + self._entitiesView?.drawingView = self._drawingView + self._entitiesView?.entityAdded = { [weak self] entity in + self?._drawingView?.onEntityAdded(entity) + } + self._entitiesView?.entityRemoved = { [weak self] entity in + self?._drawingView?.onEntityRemoved(entity) + } + self._drawingView?.getFullImage = { [weak self] in + if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() { + let size = controller.size.fitted(CGSize(width: 256.0, height: 256.0)) + + if let imageContext = DrawingContext(size: size, scale: 1.0, opaque: true, clear: false) { + imageContext.withFlippedContext { c in + let bounds = CGRect(origin: .zero, size: size) + if let cgImage = currentImage.cgImage { + c.draw(cgImage, in: bounds) + } + if let cgImage = strongSelf.drawingView.drawingImage?.cgImage { + c.draw(cgImage, in: bounds) + } + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + } + return imageContext.generateImage() + } else { + return nil + } + } else { + return nil + } + } + self._entitiesView?.selectionContainerView = self.selectionContainerView + self._entitiesView?.selectionChanged = { [weak self] entity in + if let strongSelf = self { + strongSelf.updateSelectedEntity.invoke(entity) + } + } + self._entitiesView?.requestedMenuForEntityView = { [weak self] entityView, isTopmost in + guard let strongSelf = self else { + return + } + if strongSelf.currentMenuController != nil { + if let entityView = entityView as? DrawingTextEntityView { + entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + } + return + } + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + } + })) + if let entityView = entityView as? DrawingTextEntityView { + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Edit, accessibilityLabel: strongSelf.presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + strongSelf.entitiesView.selectEntity(entityView.entity) + } + })) + } + if !isTopmost { + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_MoveForward, accessibilityLabel: strongSelf.presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid) + } + })) + } + actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Duplicate, accessibilityLabel: strongSelf.presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in + if let strongSelf = self, let entityView = entityView { + let newEntity = strongSelf.entitiesView.duplicate(entityView.entity) + strongSelf.entitiesView.selectEntity(newEntity) + } + })) + let entityFrame = entityView.convert(entityView.selectionBounds, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) + let controller = ContextMenuController(actions: actions) + strongSelf.currentMenuController = controller + strongSelf.controller?.present( + controller, + in: .window(.root), + with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf, entityFrame, strongSelf, strongSelf.bounds.insetBy(dx: 0.0, dy: 160.0)) + } else { + return nil + } + }) + ) + } + self.insertEntity.connect { [weak self] entity in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + entitiesView.prepareNewEntity(entity) + entitiesView.add(entity) + entitiesView.selectEntity(entity) + + if let entityView = entitiesView.getView(for: entity.uuid) { + if let textEntityView = entityView as? DrawingTextEntityView { + textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) + } else { + entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + + if let selectionView = entityView.selectionView { + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + } + } + } + self.deselectEntity.connect { [weak self] in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + entitiesView.selectEntity(nil) + } + } + self.updateEntitiesPlayback.connect { [weak self] play in + if let strongSelf = self, let entitiesView = strongSelf._entitiesView { + if play { + entitiesView.play() + } else { + entitiesView.pause() + } + } + } + } + return self._entitiesView! + } + + private var _selectionContainerView: DrawingSelectionContainerView? + var selectionContainerView: DrawingSelectionContainerView { + if self._selectionContainerView == nil { + self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + } + return self._selectionContainerView! + } + + private var _contentWrapperView: PortalSourceView? + var contentWrapperView: PortalSourceView { + if self._contentWrapperView == nil { + self._contentWrapperView = PortalSourceView() + } + return self._contentWrapperView! + } + + init(controller: DrawingScreen, context: AccountContext) { + self.controller = controller + self.context = context + self.updateState = ActionSlot() + self.updateColor = ActionSlot() + self.performAction = ActionSlot() + self.updateToolState = ActionSlot() + self.updateSelectedEntity = ActionSlot() + self.insertEntity = ActionSlot() + self.deselectEntity = ActionSlot() + self.updateEntitiesPlayback = ActionSlot() + self.previewBrushSize = ActionSlot() + self.dismissEyedropper = ActionSlot() + self.requestPresentColorPicker = ActionSlot() + self.toggleWithEraser = ActionSlot() + self.toggleWithPreviousTool = ActionSlot() + self.apply = ActionSlot() + self.dismiss = ActionSlot() + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) + self.textEditAccessoryHost = ComponentView() + + super.init() + + self.apply.connect { [weak self] _ in + if let strongSelf = self { + strongSelf.controller?.requestApply() + } + } + self.dismiss.connect { [weak self] _ in + if let strongSelf = self { + if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + + self?.controller?.requestDismiss() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf.controller?.requestDismiss() + } + } + } + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveKeyboardGestureRecognizer = true + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + private var currentEyedropperView: EyedropperView? + func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { + guard let controller = self.controller else { + return + } + self.entitiesView.pause() + + if controller.isVideo && retryLaterForVideo { + controller.updateVideoPlayback(false) + Queue.mainQueue().after(0.1) { + self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) + } + return + } + + guard let currentImage = controller.getCurrentImage() else { + self.entitiesView.play() + controller.updateVideoPlayback(true) + return + } + + let sourceImage = generateImage(controller.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + if let cgImage = currentImage.cgImage { + context.draw(cgImage, in: bounds) + } + if let cgImage = controller.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + controller.entitiesView.layer.render(in: context) + }, opaque: true, scale: 1.0) + guard let sourceImage = sourceImage else { + return + } + + let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: controller.drawingView, sourceImage: sourceImage) + eyedropperView.completed = { [weak self, weak controller] color in + if let strongSelf = self, let controller = controller { + strongSelf.updateColor.invoke(color) + controller.entitiesView.play() + controller.updateVideoPlayback(true) + dismissed() + } + } + eyedropperView.dismissed = { + controller.entitiesView.play() + controller.updateVideoPlayback(true) + } + eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) + controller.view.addSubview(eyedropperView) + self.currentEyedropperView = eyedropperView + } + + func dismissCurrentEyedropper() { + if let currentEyedropperView = self.currentEyedropperView { + self.currentEyedropperView = nil + currentEyedropperView.dismiss() + } + } + + private weak var colorPickerScreen: ColorPickerScreen? + func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + guard let controller = self.controller else { + return + } + self.hapticFeedback.impact(.medium) + let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in + self?.updateColor.invoke(color) + }, openEyedropper: { [weak self] in + self?.presentEyedropper(dismissed: dismissed) + }, dismissed: { + dismissed() + }) + controller.present(colorController, in: .window(.root)) + self.colorPickerScreen = colorController + } + + func dismissColorPicker() { + if let colorPickerScreen = self.colorPickerScreen { + self.colorPickerScreen = nil + colorPickerScreen.dismiss() + } + } + + private var fastColorPickerView: ColorSpectrumPickerView? + func presentFastColorPicker(sourceView: UIView) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + guard self.fastColorPickerView == nil, let superview = sourceView.superview else { + return + } + + self.hapticFeedback.impact(.medium) + + let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) + + let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) + fastColorPickerView.selected = { [weak self] color in + self?.updateColor.invoke(color) + } + let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) + sourceView.superview?.addSubview(fastColorPickerView) + + fastColorPickerView.animateIn() + + self.fastColorPickerView = fastColorPickerView + } + + func updateFastColorPickerPan(_ point: CGPoint) { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + fastColorPickerView.handlePan(point: point) + } + + func dismissFastColorPicker() { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + self.fastColorPickerView = nil + fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in + fastColorPickerView?.removeFromSuperview() + }) + } + + private weak var currentFontPicker: ContextController? + func presentFontPicker(sourceView: UIView) { + guard !self.dismissFontPicker(), let validLayout = self.validLayout?.0 else { + return + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { + entityView.textChanged = { [weak self] in + self?.dismissFontPicker() + } + } + + let fonts: [DrawingTextFont] = [ + .sanFrancisco, + .other("AmericanTypewriter", "Typewriter"), + .other("AvenirNext-DemiBoldItalic", "Avenir Next"), + .other("CourierNewPS-BoldMT", "Courier New"), + .other("Noteworthy-Bold", "Noteworthy"), + .other("Georgia-Bold", "Georgia"), + .other("Papyrus", "Papyrus"), + .other("SnellRoundhand-Bold", "Snell Roundhand") + ] + + var items: [ContextMenuItem] = [] + for font in fonts { + items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in + f.dismissWithResult(.default) + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + textEntity.font = font.font + entityView.update() + + if let (layout, orientation) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, forceUpdate: true, transition: .easeInOut(duration: 0.2)) + } + }))) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) + self.controller?.present(contextController, in: .window(.root)) + self.currentFontPicker = contextController + contextController.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @discardableResult + func dismissFontPicker() -> Bool { + if let currentFontPicker = self.currentFontPicker { + self.currentFontPicker = nil + currentFontPicker.dismiss() + return true + } + return false + } + + func animateIn() { + if let view = self.componentHost.findTaggedView(tag: topGradientTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { + buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + var delay: Double = 0.0 + for tag in colorTags { + if let view = self.componentHost.findTaggedView(tag: tag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: delay) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: delay) + delay += 0.02 + } + } + if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) { + view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + if let (layout, orientation) = self.validLayout { + self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: true, transition: .easeInOut(duration: 0.2)) + } + + if let view = self.componentHost.findTaggedView(tag: topGradientTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View { + view.animateOut() + } + if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) { + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true) + } + + for tag in colorTags { + if let view = self.componentHost.findTaggedView(tag: tag) { + view.alpha = 0.0 + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + } + } + + if let view = self.componentHost.findTaggedView(tag: toolsTag) as? ToolsComponent.View { + view.animateOut(completion: { + completion() + }) + } else if let view = self.componentHost.findTaggedView(tag: textSettingsTag) as? TextSettingsComponent.View { + view.animateOut(completion: { + completion() + }) + } + + if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View { + view.animateOut() + } + if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) { + buttonView.alpha = 0.0 + buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result == self.componentHost.view { + return nil + } + return result + } + + func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = (layout, orientation) + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: layout.intrinsicInsets.top + layout.safeInsets.top, + left: layout.safeInsets.left, + bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, + right: layout.safeInsets.right + ), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: orientation, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + var transition = transition + if isFirstTime { + transition = transition.withUserData(DrawingScreenTransition.animateIn) + } else if animateOut { + transition = transition.withUserData(DrawingScreenTransition.animateOut) + } + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + DrawingScreenComponent( + context: self.context, + isVideo: controller.isVideo, + isAvatar: controller.isAvatar, + present: { [weak self] c in + self?.controller?.present(c, in: .window(.root)) + }, + updateState: self.updateState, + updateColor: self.updateColor, + performAction: self.performAction, + updateToolState: self.updateToolState, + updateSelectedEntity: self.updateSelectedEntity, + insertEntity: self.insertEntity, + deselectEntity: self.deselectEntity, + updateEntitiesPlayback: self.updateEntitiesPlayback, + previewBrushSize: self.previewBrushSize, + dismissEyedropper: self.dismissEyedropper, + requestPresentColorPicker: self.requestPresentColorPicker, + toggleWithEraser: self.toggleWithEraser, + toggleWithPreviousTool: self.toggleWithPreviousTool, + apply: self.apply, + dismiss: self.dismiss, + presentColorPicker: { [weak self] initialColor in + self?.presentColorPicker(initialColor: initialColor) + }, + presentFastColorPicker: { [weak self] sourceView in + self?.presentFastColorPicker(sourceView: sourceView) + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + }, + presentFontPicker: { [weak self] sourceView in + self?.presentFontPicker(sourceView: sourceView) + } + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 0) + componentView.clipsToBounds = true + } + + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + + if isFirstTime { + self.animateIn() + } + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { + var isFirstTime = true + if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { + isFirstTime = false + } + UIView.performWithoutAnimation { + let accessorySize = self.textEditAccessoryHost.update( + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), + component: AnyComponent( + TextSettingsComponent( + color: textEntity.color, + style: DrawingTextStyle(style: textEntity.style), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + isEmojiKeyboard: entityView.textView.inputView != nil, + tag: nil, + fontTag: fontTag, + presentColorPicker: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + entityView.suspendEditing() + self?.presentColorPicker(initialColor: textEntity.color, dismissed: { + entityView.resumeEditing() + }) + }, + presentFastColorPicker: { [weak self] buttonTag in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { + self?.presentFastColorPicker(sourceView: buttonView) + } + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + }, + toggleStyle: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + entityView.update() + + if let (layout, orientation) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) + } + }, + toggleAlignment: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + entityView.update() + + if let (layout, orientation) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) + } + }, + presentFontPicker: { [weak self] in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) { + self?.presentFontPicker(sourceView: buttonView) + } + }, + toggleKeyboard: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismissFontPicker() + strongSelf.toggleInputMode() + } + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + if let componentView = self.textEditAccessoryHost.view { + if componentView.superview == nil { + self.textEditAccessoryView.addSubview(componentView) + } + + self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) + componentView.frame = CGRect(origin: .zero, size: accessorySize) + } + } + } + } + + private func toggleInputMode() { + guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else { + return + } + + let textView = entityView.textView + var shouldHaveInputView = false + if textView.isFirstResponder { + if textView.inputView == nil { + shouldHaveInputView = true + } + } else { + shouldHaveInputView = true + } + + if shouldHaveInputView { + let inputView = EntityInputView( + context: self.context, + isDark: true, + areCustomEmojiEnabled: true, + hideBackground: true, + forceHasPremium: true + ) + inputView.insertText = { [weak entityView] text in + entityView?.insertText(text) + } + inputView.deleteBackwards = { [weak textView] in + textView?.deleteBackward() + } + inputView.switchToKeyboard = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.toggleInputMode() + } + textView.inputView = inputView + } else { + textView.inputView = nil + } + + if textView.isFirstResponder { + textView.reloadInputViews() + } else { + textView.becomeFirstResponder() + } + + if let (layout, orientation) = self.validLayout { + self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate) + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let size: CGSize + private let originalSize: CGSize + private let isVideo: Bool + private let isAvatar: Bool + private let externalEntitiesView: DrawingEntitiesView? + + public var requestDismiss: () -> Void = {} + public var requestApply: () -> Void = {} + public var getCurrentImage: () -> UIImage? = { return nil } + public var updateVideoPlayback: (Bool) -> Void = { _ in } + + public init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { + self.context = context + self.size = size + self.originalSize = originalSize + self.isVideo = isVideo + self.isAvatar = isAvatar + + if let entitiesView = entitiesView as? DrawingEntitiesView { + self.externalEntitiesView = entitiesView + } else { + self.externalEntitiesView = nil + } + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Hide + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + public var drawingView: DrawingView { + return self.node.drawingView + } + + public var entitiesView: DrawingEntitiesView { + return self.node.entitiesView + } + + public var selectionContainerView: DrawingSelectionContainerView { + return self.node.selectionContainerView + } + + public var contentWrapperView: PortalSourceView { + return self.node.contentWrapperView + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self, context: self.context) + + super.displayNodeDidLoad() + } + + public func generateResultData() -> TGPaintingData? { + if self.drawingView.isEmpty && self.entitiesView.entities.isEmpty { + return nil + } + + let paintingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = self.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + }, opaque: false, scale: 1.0) + + var hasAnimatedEntities = false + + for entity in self.entitiesView.entities { + if entity.isAnimated { + hasAnimatedEntities = true + break + } + } + + let finalImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = paintingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + //hide animated + self.entitiesView.layer.render(in: context) + }, opaque: false, scale: 1.0) + + var image = paintingImage + var stillImage: UIImage? + if hasAnimatedEntities { + stillImage = finalImage + } else { + image = finalImage + } + + let drawingData = self.drawingView.drawingData + let entitiesData = self.entitiesView.entitiesData + + var stickers: [Any] = [] + for entity in self.entitiesView.entities { + if let sticker = entity as? DrawingStickerEntity { + let coder = PostboxEncoder() + coder.encodeRootObject(sticker.file) + stickers.append(coder.makeData()) + } else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities { + for sticker in subEntities { + let coder = PostboxEncoder() + coder.encodeRootObject(sticker.file) + stickers.append(coder.makeData()) + } + } + } + + return TGPaintingData(drawing: drawingData, entitiesData: entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities, stickers: stickers) + } + + public func resultImage() -> UIImage! { + let image = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + if let cgImage = self.drawingView.drawingImage?.cgImage { + context.draw(cgImage, in: bounds) + } + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + self.entitiesView.layer.render(in: context) + }, opaque: false, scale: 1.0) + return image + } + + public func animateOut(_ completion: @escaping (() -> Void)) { + self.selectionContainerView.alpha = 0.0 + + self.node.animateOut(completion: completion) + } + + private var orientation: UIInterfaceOrientation? + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: orientation, transition: Transition(transition)) + } + + public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { + let layout = ContainerViewLayout( + size: size, + metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact), + deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil), + intrinsicInsets: intrinsicInsets, + safeInsets: safeInsets, + additionalInsets: .zero, + statusBarHeight: statusBarHeight, + inputHeight: inputHeight, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + self.orientation = orientation + self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift new file mode 100644 index 00000000000..49cfd20c03f --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -0,0 +1,562 @@ +import Foundation +import UIKit +import Display +import AccountContext + +public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case shapeType + case drawType + case color + case lineWidth + case referenceDrawingSize + case position + case size + case rotation + case renderImage + } + + public enum ShapeType: Codable { + case rectangle + case ellipse + case star + } + + public enum DrawType: Codable { + case fill + case stroke + } + + public let uuid: UUID + public let isAnimated: Bool + + var shapeType: ShapeType + var drawType: DrawType + public var color: DrawingColor + public var lineWidth: CGFloat + + var referenceDrawingSize: CGSize + public var position: CGPoint + public var size: CGSize + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var scale: CGFloat = 1.0 + + public var renderImage: UIImage? + + init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.shapeType = shapeType + self.drawType = drawType + self.color = color + self.lineWidth = lineWidth + + self.referenceDrawingSize = .zero + self.position = .zero + self.size = CGSize(width: 1.0, height: 1.0) + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = false + self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType) + self.drawType = try container.decode(DrawType.self, forKey: .drawType) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.size = try container.decode(CGSize.self, forKey: .size) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.shapeType, forKey: .shapeType) + try container.encode(self.drawType, forKey: .drawType) + try container.encode(self.color, forKey: .color) + try container.encode(self.lineWidth, forKey: .lineWidth) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.size, forKey: .size) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.size = self.size + newEntity.rotation = self.rotation + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingSimpleShapeEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + self.renderImage = (self.currentEntityView as? DrawingSimpleShapeEntityView)?.getRenderImage() + } +} + +final class DrawingSimpleShapeEntityView: DrawingEntityView { + private var shapeEntity: DrawingSimpleShapeEntity { + return self.entity as! DrawingSimpleShapeEntity + } + + private var currentShape: DrawingSimpleShapeEntity.ShapeType? + private var currentSize: CGSize? + + private let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingSimpleShapeEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(animated: Bool) { + let shapeType = self.shapeEntity.shapeType + let size = self.shapeEntity.size + + self.center = self.shapeEntity.position + self.bounds = CGRect(origin: .zero, size: size) + self.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation) + + if shapeType != self.currentShape || size != self.currentSize { + self.currentShape = shapeType + self.currentSize = size + self.shapeLayer.frame = self.bounds + + let rect = CGRect(origin: .zero, size: size).insetBy(dx: maxLineWidth * 0.5, dy: maxLineWidth * 0.5) + switch shapeType { + case .rectangle: + self.shapeLayer.path = CGPath(rect: rect, transform: nil) + case .ellipse: + self.shapeLayer.path = CGPath(ellipseIn: rect, transform: nil) + case .star: + self.shapeLayer.path = CGPath.star(in: rect, extrusion: size.width * 0.2, points: 5) + } + } + + switch self.shapeEntity.drawType { + case .fill: + self.shapeLayer.fillColor = self.shapeEntity.color.toCGColor() + self.shapeLayer.strokeColor = UIColor.clear.cgColor + case .stroke: + let minLineWidth = max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.01) + let maxLineWidth = self.maxLineWidth + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.shapeEntity.lineWidth + + self.shapeLayer.fillColor = UIColor.clear.cgColor + self.shapeLayer.strokeColor = self.shapeEntity.color.toCGColor() + self.shapeLayer.lineWidth = lineWidth + } + + super.update(animated: animated) + } + + fileprivate var visualLineWidth: CGFloat { + return self.shapeLayer.lineWidth + } + + fileprivate var maxLineWidth: CGFloat { + return max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.05) + } + + fileprivate var minimumSize: CGSize { + let minSize = min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) + return CGSize(width: minSize * 0.2, height: minSize * 0.2) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let lineWidth = self.maxLineWidth * 0.5 + let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth) + if expandedBounds.contains(point) { + return true + } + return false + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if case .stroke = self.shapeEntity.drawType, var path = self.shapeLayer.path { + path = path.copy(strokingWithWidth: self.maxLineWidth * 0.8, lineCap: .square, lineJoin: .bevel, miterLimit: 0.0) + if path.contains(point) { + return true + } else { + return false + } + } else { + return super.precisePoint(inside: point) + } + } + + override func updateSelectionView() { + super.updateSelectionView() + + guard let selectionView = self.selectionView as? DrawingSimpleShapeEntititySelectionView else { + return + } + +// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 +// selectionView.scale = scale + + selectionView.transform = CGAffineTransformMakeRotation(self.shapeEntity.rotation) + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingSimpleShapeEntititySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) + self.drawHierarchy(in: rect, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + override var selectionBounds: CGRect { + return self.bounds.insetBy(dx: self.maxLineWidth * 0.5, dy: self.maxLineWidth * 0.5) + } +} + +func gestureIsTracking(_ gestureRecognizer: UIPanGestureRecognizer) -> Bool { + return [.began, .changed].contains(gestureRecognizer.state) +} + +final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let leftHandle = SimpleShapeLayer() + private let topLeftHandle = SimpleShapeLayer() + private let topHandle = SimpleShapeLayer() + private let topRightHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + private let bottomLeftHandle = SimpleShapeLayer() + private let bottomHandle = SimpleShapeLayer() + private let bottomRightHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + + self.snapTool.onSnapXUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToXAxis(snapped) + } + } + + self.snapTool.onSnapYUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToYAxis(snapped) + } + } + + self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToAngle(snappedAngle) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 5.5 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else { + return + } + let isAspectLocked = [.star].contains(entity.shapeType) + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + return + } + } + } + self.currentHandle = self.layer + case .changed: + let delta = gestureRecognizer.translation(in: entityView.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedSize = entity.size + var updatedPosition = entity.position + + let minimumSize = entityView.minimumSize + + if self.currentHandle === self.leftHandle { + let deltaX = delta.x * cos(entity.rotation) + let deltaY = delta.x * sin(entity.rotation) + + updatedSize.width = max(minimumSize.width, updatedSize.width - deltaX) + updatedPosition.x -= deltaX * -0.5 + updatedPosition.y -= deltaY * -0.5 + + if isAspectLocked { + updatedSize.height = updatedSize.width + } + } else if self.currentHandle === self.rightHandle { + let deltaX = delta.x * cos(entity.rotation) + let deltaY = delta.x * sin(entity.rotation) + + updatedSize.width = max(minimumSize.width, updatedSize.width + deltaX) + print(updatedSize.width) + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.height = updatedSize.width + } + } else if self.currentHandle === self.topHandle { + let deltaX = delta.y * sin(entity.rotation) + let deltaY = delta.y * cos(entity.rotation) + + updatedSize.height = max(minimumSize.height, updatedSize.height - deltaY) + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.width = updatedSize.height + } + } else if self.currentHandle === self.bottomHandle { + let deltaX = delta.y * sin(entity.rotation) + let deltaY = delta.y * cos(entity.rotation) + + updatedSize.height = max(minimumSize.height, updatedSize.height + deltaY) + updatedPosition.x += deltaX * 0.5 + updatedPosition.y += deltaY * 0.5 + if isAspectLocked { + updatedSize.width = updatedSize.height + } + } else if self.currentHandle === self.topLeftHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: delta.x) + } + + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) + updatedPosition.x -= delta.x * -0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.topRightHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: -delta.x) + } + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) + updatedPosition.x += delta.x * 0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height - delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomLeftHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: -delta.x) + } + updatedSize.width = max(minimumSize.width, updatedSize.width - delta.x) + updatedPosition.x -= delta.x * -0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.bottomRightHandle { + var delta = delta + if isAspectLocked { + delta = CGPoint(x: delta.x, y: delta.x) + } + updatedSize.width = max(minimumSize.width, updatedSize.width + delta.x) + updatedPosition.x += delta.x * 0.5 + updatedSize.height = max(minimumSize.height, updatedSize.height + delta.y) + updatedPosition.y += delta.y * 0.5 + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + } + + entity.size = updatedSize + entity.position = updatedPosition + entityView.update(animated: false) + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + self.snapTool.reset() + case .cancelled: + self.snapTool.reset() + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + default: + break + } + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.topLeftHandle, + self.topHandle, + self.topRightHandle, + self.rightHandle, + self.bottomLeftHandle, + self.bottomHandle, + self.bottomRightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.topLeftHandle.position = CGPoint(x: inset, y: inset) + self.topHandle.position = CGPoint(x: self.bounds.midX, y: inset) + self.topRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: inset) + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + self.bottomLeftHandle.position = CGPoint(x: inset, y: self.bounds.maxY - inset) + self.bottomHandle.position = CGPoint(x: self.bounds.midX, y: self.bounds.maxY - inset) + self.bottomRightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.maxY - inset) + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift new file mode 100644 index 00000000000..5f829a9e975 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -0,0 +1,759 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import AccountContext + +public final class DrawingStickerEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case isAnimated + case file + case referenceDrawingSize + case position + case scale + case rotation + case mirrored + } + + public let uuid: UUID + public let isAnimated: Bool + public let file: TelegramMediaFile + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var scale: CGFloat + public var rotation: CGFloat + public var mirrored: Bool + + public var color: DrawingColor = DrawingColor.clear + public var lineWidth: CGFloat = 0.0 + + public var center: CGPoint { + return self.position + } + + public var baseSize: CGSize { + let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.4) + return CGSize(width: size, height: size) + } + + init(file: TelegramMediaFile) { + self.uuid = UUID() + self.isAnimated = file.isAnimatedSticker || file.isVideoSticker + + self.file = file + + self.referenceDrawingSize = .zero + self.position = CGPoint() + self.scale = 1.0 + self.rotation = 0.0 + self.mirrored = false + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = try container.decode(Bool.self, forKey: .isAnimated) + self.file = try container.decode(TelegramMediaFile.self, forKey: .file) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + self.mirrored = try container.decode(Bool.self, forKey: .mirrored) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.isAnimated, forKey: .isAnimated) + try container.encode(self.file, forKey: .file) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + try container.encode(self.mirrored, forKey: .mirrored) + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingStickerEntity(file: self.file) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.scale = self.scale + newEntity.rotation = self.rotation + newEntity.mirrored = self.mirrored + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingStickerEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + } +} + +final class DrawingStickerEntityView: DrawingEntityView { + private var stickerEntity: DrawingStickerEntity { + return self.entity as! DrawingStickerEntity + } + + var started: ((Double) -> Void)? + + private var currentSize: CGSize? + private var dimensions: CGSize? + + private let imageNode: TransformImageNode + private var animationNode: AnimatedStickerNode? + + private var didSetUpAnimationNode = false + private let stickerFetchedDisposable = MetaDisposable() + private let cachedDisposable = MetaDisposable() + + private var isVisible = true + private var isPlaying = false + + init(context: AccountContext, entity: DrawingStickerEntity) { + self.imageNode = TransformImageNode() + + super.init(context: context, entity: entity) + + self.addSubview(self.imageNode.view) + + self.setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stickerFetchedDisposable.dispose() + self.cachedDisposable.dispose() + } + + private var file: TelegramMediaFile { + return (self.entity as! DrawingStickerEntity).file + } + + private func setup() { + if let dimensions = self.file.dimensions { + if self.file.isAnimatedSticker || self.file.isVideoSticker || self.file.mimeType == "video/webm" { + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self, weak animationNode] in + self?.imageNode.isHidden = true + + if let animationNode = animationNode { + let _ = (animationNode.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] status in + self?.started?(status.duration) + }) + } + } + self.addSubnode(animationNode) + } + let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + self.imageNode.isHidden = false + self.didSetUpAnimationNode = false + } + self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: self.file, small: false, synchronousLoad: false)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start()) + } + + self.dimensions = dimensions.cgSize + self.setNeedsLayout() + } + } + + override func play() { + self.isVisible = true + self.applyVisibility() + } + + override func pause() { + self.isVisible = false + self.applyVisibility() + } + + override func seek(to timestamp: Double) { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(timestamp)) + } + + override func resetToStart() { + self.isVisible = false + self.isPlaying = false + self.animationNode?.seekTo(.timestamp(0.0)) + } + + override func updateVisibility(_ visibility: Bool) { + self.isVisible = visibility + self.applyVisibility() + } + + private func applyVisibility() { + let isPlaying = self.isVisible + if self.isPlaying != isPlaying { + self.isPlaying = isPlaying + + if isPlaying && !self.didSetUpAnimationNode { + self.didSetUpAnimationNode = true + let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) + let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker || self.file.mimeType == "video/webm") + self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + + self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) + |> deliverOn(Queue.concurrentDefaultQueue())).start()) + } + self.animationNode?.visibility = isPlaying + } + } + + private var didApplyVisibility = false + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + + if size.width > 0 && self.currentSize != size { + self.currentSize = size + + let sideSize: CGFloat = size.width + let boundingSize = CGSize(width: sideSize, height: sideSize) + + if let dimensions = self.dimensions { + let imageSize = dimensions.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + + if !self.didApplyVisibility { + self.didApplyVisibility = true + self.applyVisibility() + } + } + self.update(animated: false) + } + } + } + + override func update(animated: Bool) { + guard let dimensions = self.stickerEntity.file.dimensions?.cgSize else { + return + } + self.center = self.stickerEntity.position + + let size = self.stickerEntity.baseSize + + self.bounds = CGRect(origin: .zero, size: dimensions.aspectFitted(size)) + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) + + var transform = CATransform3DIdentity + + if self.stickerEntity.mirrored { + transform = CATransform3DRotate(transform, .pi, 0.0, 1.0, 0.0) + transform.m34 = -1.0 / self.imageNode.frame.width + } + + if animated { + UIView.animate(withDuration: 0.25, delay: 0.0) { + self.imageNode.transform = transform + self.animationNode?.transform = transform + } + } else { + self.imageNode.transform = transform + self.animationNode?.transform = transform + } + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingStickerEntititySelectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.stickerEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.stickerEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingStickerEntititySelectionView() + selectionView.entityView = self + return selectionView + } +} + +final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let border = SimpleShapeLayer() + private let leftHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.rightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.border.lineCap = .round + self.border.fillColor = UIColor.clear.cgColor + self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + self.border.shadowColor = UIColor.black.cgColor + self.border.shadowRadius = 1.0 + self.border.shadowOpacity = 0.5 + self.border.shadowOffset = CGSize() + self.layer.addSublayer(self.border) + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + + self.snapTool.onSnapXUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToXAxis(snapped) + } + } + + self.snapTool.onSnapYUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToYAxis(snapped) + } + } + + self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToAngle(snappedAngle) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 18.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + return + } + } + } + self.currentHandle = self.layer + case .changed: + let delta = gestureRecognizer.translation(in: entityView.superview) + let parentLocation = gestureRecognizer.location(in: self.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedPosition = entity.position + var updatedScale = entity.scale + var updatedRotation = entity.rotation + if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { + var deltaX = gestureRecognizer.translation(in: self).x + if self.currentHandle === self.leftHandle { + deltaX *= -1.0 + } + let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width + updatedScale *= scaleDelta + + let newAngle: CGFloat + if self.currentHandle === self.leftHandle { + newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) + } else { + newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) + } + + // let delta = newAngle - updatedRotation + updatedRotation = newAngle// self.snapTool.update(entityView: entityView, velocity: 0.0, delta: delta, updatedRotation: newAngle) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + } + + entity.position = updatedPosition + entity.scale = updatedScale + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended, .cancelled: + self.snapTool.reset() + if self.currentHandle != nil { + self.snapTool.rotationReset() + } + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.scale = entity.scale * scale + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + default: + break + } + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset - 10.0 + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.rightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + + + let radius = (self.bounds.width - inset * 2.0) / 2.0 + let circumference: CGFloat = 2.0 * .pi * radius + let count = 10 + let relativeDashLength: CGFloat = 0.25 + let dashLength = circumference / CGFloat(count) + self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] + + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath + } +} + +private let snapTimeout = 1.0 + +class DrawingEntitySnapTool { + private var xState: (skipped: CGFloat, waitForLeave: Bool)? + private var yState: (skipped: CGFloat, waitForLeave: Bool)? + private var rotationState: (angle: CGFloat, skipped: CGFloat, waitForLeave: Bool)? + + var onSnapXUpdated: (Bool) -> Void = { _ in } + var onSnapYUpdated: (Bool) -> Void = { _ in } + var onSnapRotationUpdated: (CGFloat?) -> Void = { _ in } + + var previousXSnapTimestamp: Double? + var previousYSnapTimestamp: Double? + var previousRotationSnapTimestamp: Double? + + func reset() { + self.xState = nil + self.yState = nil + + self.onSnapXUpdated(false) + self.onSnapYUpdated(false) + } + + func rotationReset() { + self.rotationState = nil + self.onSnapRotationUpdated(nil) + } + + func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) { + self.xState = nil + self.yState = nil + + let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 + let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 + + if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { + if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta { + self.xState = (0.0, true) + } + + if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta { + self.yState = (0.0, true) + } + } + } + + func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint { + var updatedPosition = updatedPosition + + let currentTimestamp = CACurrentMediaTime() + + let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 + let snapXVelocity: CGFloat = snapXDelta * 12.0 + let snapXSkipTranslation: CGFloat = snapXDelta * 2.0 + + if abs(velocity.x) < snapXVelocity || self.xState?.waitForLeave == true { + if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { + if let (skipped, waitForLeave) = self.xState { + if waitForLeave { + if updatedPosition.x > snapLocation.x - snapXDelta * 2.0 && updatedPosition.x < snapLocation.x + snapXDelta * 2.0 { + + } else { + self.xState = nil + } + } else if abs(skipped) < snapXSkipTranslation { + self.xState = (skipped + delta.x, false) + updatedPosition.x = snapLocation.x + } else { + self.xState = (snapXSkipTranslation, true) + self.onSnapXUpdated(false) + } + } else { + if updatedPosition.x > snapLocation.x - snapXDelta && updatedPosition.x < snapLocation.x + snapXDelta { + if let previousXSnapTimestamp, currentTimestamp - previousXSnapTimestamp < snapTimeout { + + } else { + self.previousXSnapTimestamp = currentTimestamp + self.xState = (0.0, false) + updatedPosition.x = snapLocation.x + self.onSnapXUpdated(true) + } + } + } + } + } else { + self.xState = nil + self.onSnapXUpdated(false) + } + + let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 + let snapYVelocity: CGFloat = snapYDelta * 12.0 + let snapYSkipTranslation: CGFloat = snapYDelta * 2.0 + + if abs(velocity.y) < snapYVelocity || self.yState?.waitForLeave == true { + if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { + if let (skipped, waitForLeave) = self.yState { + if waitForLeave { + if updatedPosition.y > snapLocation.y - snapYDelta * 2.0 && updatedPosition.y < snapLocation.y + snapYDelta * 2.0 { + + } else { + self.yState = nil + } + } else if abs(skipped) < snapYSkipTranslation { + self.yState = (skipped + delta.y, false) + updatedPosition.y = snapLocation.y + } else { + self.yState = (snapYSkipTranslation, true) + self.onSnapYUpdated(false) + } + } else { + if updatedPosition.y > snapLocation.y - snapYDelta && updatedPosition.y < snapLocation.y + snapYDelta { + if let previousYSnapTimestamp, currentTimestamp - previousYSnapTimestamp < snapTimeout { + + } else { + self.previousYSnapTimestamp = currentTimestamp + self.yState = (0.0, false) + updatedPosition.y = snapLocation.y + self.onSnapYUpdated(true) + } + } + } + } + } else { + self.yState = nil + self.onSnapYUpdated(false) + } + + return updatedPosition + } + + private let snapRotations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] + func maybeSkipFromStart(entityView: DrawingEntityView, rotation: CGFloat) { + self.rotationState = nil + + let snapDelta: CGFloat = 0.25 + for snapRotation in self.snapRotations { + let snapRotation = snapRotation * .pi + if rotation > snapRotation - snapDelta && rotation < snapRotation + snapDelta { + self.rotationState = (snapRotation, 0.0, true) + break + } + } + } + + func update(entityView: DrawingEntityView, velocity: CGFloat, delta: CGFloat, updatedRotation: CGFloat) -> CGFloat { + var updatedRotation = updatedRotation + if updatedRotation < 0.0 { + updatedRotation = 2.0 * .pi + updatedRotation + } else if updatedRotation > 2.0 * .pi { + while updatedRotation > 2.0 * .pi { + updatedRotation -= 2.0 * .pi + } + } + + let currentTimestamp = CACurrentMediaTime() + + let snapDelta: CGFloat = 0.1 + let snapVelocity: CGFloat = snapDelta * 5.0 + let snapSkipRotation: CGFloat = snapDelta * 2.0 + + if abs(velocity) < snapVelocity || self.rotationState?.waitForLeave == true { + if let (snapRotation, skipped, waitForLeave) = self.rotationState { + if waitForLeave { + if updatedRotation > snapRotation - snapDelta * 2.0 && updatedRotation < snapRotation + snapDelta { + + } else { + self.rotationState = nil + } + } else if abs(skipped) < snapSkipRotation { + self.rotationState = (snapRotation, skipped + delta, false) + updatedRotation = snapRotation + } else { + self.rotationState = (snapRotation, snapSkipRotation, true) + self.onSnapRotationUpdated(nil) + } + } else { + for snapRotation in self.snapRotations { + let snapRotation = snapRotation * .pi + if updatedRotation > snapRotation - snapDelta && updatedRotation < snapRotation + snapDelta { + if let previousRotationSnapTimestamp, currentTimestamp - previousRotationSnapTimestamp < snapTimeout { + + } else { + self.previousRotationSnapTimestamp = currentTimestamp + self.rotationState = (snapRotation, 0.0, false) + updatedRotation = snapRotation + self.onSnapRotationUpdated(snapRotation) + } + break + } + } + } + } else { + self.rotationState = nil + self.onSnapRotationUpdated(nil) + } + + return updatedRotation + } +} diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift new file mode 100644 index 00000000000..baaddd7c01e --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -0,0 +1,1306 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TextFormat +import EmojiTextAttachmentView + +public final class DrawingTextEntity: DrawingEntity, Codable { + final class CustomEmojiAttribute: Codable { + private enum CodingKeys: String, CodingKey { + case attribute + case rangeOrigin + case rangeLength + } + let attribute: ChatTextInputTextCustomEmojiAttribute + let range: NSRange + + init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) { + self.attribute = attribute + self.range = range + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute) + + let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin) + let rangeLength = try container.decode(Int.self, forKey: .rangeLength) + self.range = NSMakeRange(rangeOrigin, rangeLength) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.attribute, forKey: .attribute) + try container.encode(self.range.location, forKey: .rangeOrigin) + try container.encode(self.range.length, forKey: .rangeLength) + } + } + + private enum CodingKeys: String, CodingKey { + case uuid + case text + case textAttributes + case style + case font + case alignment + case fontSize + case color + case referenceDrawingSize + case position + case width + case scale + case rotation + case renderImage + case renderSubEntities + } + + enum Style: Codable { + case regular + case filled + case semi + case stroke + + init(style: DrawingTextEntity.Style) { + switch style { + case .regular: + self = .regular + case .filled: + self = .filled + case .semi: + self = .semi + case .stroke: + self = .stroke + } + } + } + + enum Font: Codable { + case sanFrancisco + case other(String, String) + } + + enum Alignment: Codable { + case left + case center + case right + + var alignment: NSTextAlignment { + switch self { + case .left: + return .left + case .center: + return .center + case .right: + return .right + } + } + } + + public var uuid: UUID + public var isAnimated: Bool { + var isAnimated = false + self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + isAnimated = true + } + }) + return isAnimated + } + + var text: NSAttributedString + var style: Style + var font: Font + var alignment: Alignment + var fontSize: CGFloat + public var color: DrawingColor + public var lineWidth: CGFloat = 0.0 + + var referenceDrawingSize: CGSize + public var position: CGPoint + var width: CGFloat + public var scale: CGFloat + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingStickerEntity]? + + init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) { + self.uuid = UUID() + + self.text = text + self.style = style + self.font = font + self.alignment = alignment + self.fontSize = fontSize + self.color = color + + self.referenceDrawingSize = .zero + self.position = .zero + self.width = 100.0 + self.scale = 1.0 + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + let text = try container.decode(String.self, forKey: .text) + + let attributedString = NSMutableAttributedString(string: text) + let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes) + for attribute in textAttributes { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range) + } + self.text = attributedString + + self.style = try container.decode(Style.self, forKey: .style) + self.font = try container.decode(Font.self, forKey: .font) + self.alignment = try container.decode(Alignment.self, forKey: .alignment) + self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.width = try container.decode(CGFloat.self, forKey: .width) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { + self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.text.string, forKey: .text) + + var textAttributes: [CustomEmojiAttribute] = [] + self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + textAttributes.append(CustomEmojiAttribute(attribute: value, range: range)) + } + }) + try container.encode(textAttributes, forKey: .textAttributes) + + try container.encode(self.style, forKey: .style) + try container.encode(self.font, forKey: .font) + try container.encode(self.alignment, forKey: .alignment) + try container.encode(self.fontSize, forKey: .fontSize) + try container.encode(self.color, forKey: .color) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.width, forKey: .width) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + if let renderSubEntities = self.renderSubEntities { + let codableEntities: [CodableDrawingEntity] = renderSubEntities.map { .sticker($0) } + try container.encode(codableEntities, forKey: .renderSubEntities) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.width = self.width + newEntity.scale = self.scale + newEntity.rotation = self.rotation + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingTextEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + self.renderImage = (self.currentEntityView as? DrawingTextEntityView)?.getRenderImage() + self.renderSubEntities = (self.currentEntityView as? DrawingTextEntityView)?.getRenderSubEntities() + } +} + +final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { + private var textEntity: DrawingTextEntity { + return self.entity as! DrawingTextEntity + } + + let textView: DrawingTextView + var customEmojiContainerView: CustomEmojiContainerView? + var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? + + var textChanged: () -> Void = {} + + init(context: AccountContext, entity: DrawingTextEntity) { + self.textView = DrawingTextView(frame: .zero) + self.textView.clipsToBounds = false + + self.textView.backgroundColor = .clear + self.textView.isEditable = false + self.textView.isSelectable = false + self.textView.contentInset = .zero + self.textView.showsHorizontalScrollIndicator = false + self.textView.showsVerticalScrollIndicator = false + self.textView.scrollsToTop = false + self.textView.isScrollEnabled = false + self.textView.textContainerInset = .zero + self.textView.minimumZoomScale = 1.0 + self.textView.maximumZoomScale = 1.0 + self.textView.keyboardAppearance = .dark + self.textView.autocorrectionType = .no + self.textView.spellCheckingType = .no + + super.init(context: context, entity: entity) + + self.textView.delegate = self + self.addSubview(self.textView) + + self.emojiViewProvider = { [weak self] emoji in + guard let strongSelf = self else { + return UIView() + } + + let pointSize: CGFloat = 128.0 + return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) + } + + self.update(animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var isSuspended = false + private var _isEditing = false + var isEditing: Bool { + return self._isEditing || self.isSuspended + } + + private var previousEntity: DrawingTextEntity? + private var fadeView: UIView? + + @objc private func fadePressed() { + self.endEditing() + } + + private var emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + func updateEntities() { + self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer) + + var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + + var shouldRepeat = false + if let attributedText = self.textView.attributedText { + let beginning = self.textView.beginningOfDocument + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { + if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) { + let rect = self.textView.firstRect(for: textRange) + customEmojiRects.append((rect, value)) + if rect.origin.x.isInfinite { + shouldRepeat = true + } + } + } + }) + } + + let color = self.textEntity.color.toUIColor() + let textColor: UIColor + switch self.textEntity.style { + case .regular: + textColor = color + case .filled: + textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + case .semi: + textColor = color + case .stroke: + textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + } + + self.emojiRects = customEmojiRects + if !customEmojiRects.isEmpty && !shouldRepeat { + let customEmojiContainerView: CustomEmojiContainerView + if let current = self.customEmojiContainerView { + customEmojiContainerView = current + } else { + customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in + guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else { + return nil + } + return emojiViewProvider(emoji) + }) + customEmojiContainerView.isUserInteractionEnabled = false + customEmojiContainerView.center = customEmojiContainerView.center + self.addSubview(customEmojiContainerView) + self.customEmojiContainerView = customEmojiContainerView + } + + customEmojiContainerView.update(fontSize: self.displayFontSize * 0.78, textColor: textColor, emojiRects: customEmojiRects) + } else if let customEmojiContainerView = self.customEmojiContainerView { + customEmojiContainerView.removeFromSuperview() + self.customEmojiContainerView = nil + } + + if shouldRepeat { + Queue.mainQueue().after(0.01) { + self.updateEntities() + } + } + } + + func beginEditing(accessoryView: UIView?) { + self._isEditing = true + if !self.textEntity.text.string.isEmpty { + let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity + previousEntity?.uuid = self.textEntity.uuid + self.previousEntity = previousEntity + } + + self.update(animated: false) + + if let superview = self.superview { + let fadeView = UIButton(frame: CGRect(origin: .zero, size: superview.frame.size)) + fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside) + superview.insertSubview(fadeView, belowSubview: self) + self.fadeView = fadeView + fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + self.textView.inputAccessoryView = accessoryView + + self.textView.isEditable = true + self.textView.isSelectable = true + + self.textView.window?.makeKey() + self.textView.becomeFirstResponder() + + UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) { + self.transform = .identity + if let superview = self.superview { + self.center = CGPoint(x: superview.bounds.width / 2.0, y: superview.bounds.height / 2.0) + } + } + + if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + selectionView.alpha = 0.0 + if !self.textEntity.text.string.isEmpty { + selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + + func endEditing(reset: Bool = false) { + self._isEditing = false + self.textView.resignFirstResponder() + self.textView.inputView = nil + self.textView.inputAccessoryView = nil + + self.textView.isEditable = false + self.textView.isSelectable = false + + if reset { + if let previousEntity = self.previousEntity { + self.textEntity.color = previousEntity.color + self.textEntity.style = previousEntity.style + self.textEntity.alignment = previousEntity.alignment + self.textEntity.font = previousEntity.font + self.textEntity.text = previousEntity.text + + self.previousEntity = nil + } else { + self.containerView?.remove(uuid: self.textEntity.uuid) + } + } else { +// self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines) + if self.textEntity.text.string.isEmpty { + self.containerView?.remove(uuid: self.textEntity.uuid) + } + } + + if let fadeView = self.fadeView { + self.fadeView = nil + fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fadeView] _ in + fadeView?.removeFromSuperview() + }) + } + + UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.0) { + self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) + self.center = self.textEntity.position + } + self.update(animated: false) + + if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + selectionView.alpha = 1.0 + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + func suspendEditing() { + self.isSuspended = true + self.textView.resignFirstResponder() + + if let fadeView = self.fadeView { + fadeView.alpha = 0.0 + fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + + func resumeEditing() { + self.isSuspended = false + self.textView.becomeFirstResponder() + + if let fadeView = self.fadeView { + fadeView.alpha = 1.0 + fadeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + + func textViewDidChange(_ textView: UITextView) { + guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else { + return + } + let range = NSMakeRange(0, updatedText.length) + updatedText.removeAttribute(.font, range: range) + updatedText.removeAttribute(.paragraphStyle, range: range) + updatedText.removeAttribute(.foregroundColor, range: range) + + self.textEntity.text = updatedText + + self.sizeToFit() + self.update(afterAppendingEmoji: true) + + self.textChanged() + } + + func insertText(_ text: NSAttributedString) { + guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else { + return + } + let range = NSMakeRange(0, updatedText.length) + updatedText.removeAttribute(.font, range: range) + updatedText.removeAttribute(.paragraphStyle, range: range) + updatedText.removeAttribute(.foregroundColor, range: range) + + let previousSelectedRange = self.textView.selectedRange + updatedText.replaceCharacters(in: self.textView.selectedRange, with: text) + + self.textEntity.text = updatedText + + self.update(animated: false, afterAppendingEmoji: true) + + self.textView.selectedRange = NSMakeRange(previousSelectedRange.location + previousSelectedRange.length + text.length, 0) + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude)) + result.width = max(224.0, ceil(result.width) + 20.0) + result.height = ceil(result.height) //+ 20.0 + (self.textView.font?.pointSize ?? 0.0) // * _font.sizeCorrection; + return result; + } + + override func sizeToFit() { + let center = self.center + let transform = self.transform + self.transform = .identity + super.sizeToFit() + self.center = center + self.transform = transform + + //entity changed + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.textView.frame = self.bounds + } + + private var displayFontSize: CGFloat { + let minFontSize = max(10.0, max(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.025) + let maxFontSize = max(10.0, max(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.25) + let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize + return fontSize + } + + private func updateText(keepSelectedRange: Bool = false) { + guard let text = self.textEntity.text.mutableCopy() as? NSMutableAttributedString else { + return + } + let range = NSMakeRange(0, text.length) + let fontSize = self.displayFontSize + + self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) + + if let (font, name) = availableFonts[text.string.lowercased()] { + self.textEntity.font = .other(font, name) + } + + var font: UIFont + switch self.textEntity.font { + case .sanFrancisco: + font = Font.with(size: fontSize, design: .round, weight: .semibold) + case let .other(fontName, _): + font = UIFont(name: fontName, size: fontSize) ?? Font.with(size: fontSize, design: .round, weight: .semibold) + } + + text.addAttribute(.font, value: font, range: range) + self.textView.font = font + + let color = self.textEntity.color.toUIColor() + let textColor: UIColor + switch self.textEntity.style { + case .regular: + textColor = color + case .filled: + textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + case .semi: + textColor = color + case .stroke: + textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + } + text.addAttribute(.foregroundColor, value: textColor, range: range) + + text.enumerateAttributes(in: range) { attributes, subrange, _ in + if let _ = attributes[ChatTextInputAttributes.customEmoji] { + text.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange) + } + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = self.textEntity.alignment.alignment + text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + let previousRange = self.textView.selectedRange + self.textView.attributedText = text + if keepSelectedRange { + self.textView.selectedRange = previousRange + } + } + + override func update(animated: Bool = false) { + self.update(animated: animated, afterAppendingEmoji: false) + } + + func update(animated: Bool = false, afterAppendingEmoji: Bool = false) { + if !self.isEditing { + self.center = self.textEntity.position + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.textEntity.rotation), self.textEntity.scale, self.textEntity.scale) + } + + let color = self.textEntity.color.toUIColor() + switch self.textEntity.style { + case .regular: + self.textView.textColor = color + self.textView.strokeColor = nil + self.textView.frameColor = nil + case .filled: + self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + self.textView.strokeColor = nil + self.textView.frameColor = color + case .semi: + self.textView.textColor = color + self.textView.strokeColor = nil + self.textView.frameColor = color.lightness > 0.7 ? UIColor(rgb: 0x000000, alpha: 0.75) : UIColor(rgb: 0xffffff, alpha: 0.75) + case .stroke: + self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white + self.textView.strokeColor = color + self.textView.frameColor = nil + } + + if case .regular = self.textEntity.style { + self.textView.layer.shadowColor = UIColor.black.cgColor + self.textView.layer.shadowOffset = CGSize(width: 0.0, height: 4.0) + self.textView.layer.shadowOpacity = 0.4 + self.textView.layer.shadowRadius = 4.0 + } else { + self.textView.layer.shadowColor = nil + self.textView.layer.shadowOffset = .zero + self.textView.layer.shadowOpacity = 0.0 + self.textView.layer.shadowRadius = 0.0 + } + + self.updateText(keepSelectedRange: afterAppendingEmoji) + + self.sizeToFit() + + Queue.mainQueue().after(afterAppendingEmoji ? 0.01 : 0.001) { + self.updateEntities() + } + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingTextEntititySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) + self.textView.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func getRenderSubEntities() -> [DrawingStickerEntity] { + let textSize = self.textView.bounds.size + let textPosition = self.textEntity.position + let scale = self.textEntity.scale + let rotation = self.textEntity.rotation + + let itemSize: CGFloat = floor(24.0 * self.displayFontSize * 0.78 / 17.0) + + var entities: [DrawingStickerEntity] = [] + for (emojiRect, emojiAttribute) in self.emojiRects { + guard let file = emojiAttribute.file else { + continue + } + let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) + + let entity = DrawingStickerEntity(file: file) + entity.referenceDrawingSize = CGSize(width: itemSize * 2.5, height: itemSize * 2.5) + entity.scale = scale + entity.position = textPosition.offsetBy( + dx: (emojiTextPosition.x * cos(rotation) + emojiTextPosition.y * sin(rotation)) * scale, + dy: (emojiTextPosition.y * cos(rotation) + emojiTextPosition.x * sin(rotation)) * scale + ) + entity.rotation = rotation + entities.append(entity) + } + return entities + } +} + +final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let border = SimpleShapeLayer() + private let leftHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.rightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.border.lineCap = .round + self.border.fillColor = UIColor.clear.cgColor + self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor + self.layer.addSublayer(self.border) + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + + self.snapTool.onSnapXUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToXAxis(snapped) + } + } + + self.snapTool.onSnapYUpdated = { [weak self] snapped in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToYAxis(snapped) + } + } + + self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in + if let strongSelf = self, let entityView = strongSelf.entityView { + entityView.onSnapToAngle(snappedAngle) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 15.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let entityView = self.entityView as? DrawingTextEntityView, entityView.isEditing { + return false + } + return true + } + + private let snapTool = DrawingEntitySnapTool() + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + return + } + } + } + self.currentHandle = self.layer + case .changed: + let delta = gestureRecognizer.translation(in: entityView.superview) + let parentLocation = gestureRecognizer.location(in: self.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedScale = entity.scale + var updatedPosition = entity.position + var updatedRotation = entity.rotation + + if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { + var deltaX = gestureRecognizer.translation(in: self).x + if self.currentHandle === self.leftHandle { + deltaX *= -1.0 + } + let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width + updatedScale = max(0.01, updatedScale * scaleDelta) + + let newAngle: CGFloat + if self.currentHandle === self.leftHandle { + newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) + } else { + newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) + } + + //let delta = newAngle - updatedRotation + updatedRotation = newAngle //" self.snapTool.update(entityView: entityView, velocity: 0.0, delta: delta, updatedRotation: newAngle) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + } + + entity.scale = updatedScale + entity.position = updatedPosition + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended, .cancelled: + self.snapTool.reset() + if self.currentHandle != nil { + self.snapTool.rotationReset() + } + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView as? DrawingTextEntityView, !entityView.isEditing, let entity = entityView.entity as? DrawingTextEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + let scale = gestureRecognizer.scale + entity.scale = max(0.1, entity.scale * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView as? DrawingTextEntityView, !entityView.isEditing, let entity = entityView.entity as? DrawingTextEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + default: + break + } + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset - 10.0 + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.rightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + + let width: CGFloat = self.bounds.width - inset * 2.0 + let height: CGFloat = self.bounds.height - inset * 2.0 + let cornerRadius: CGFloat = 12.0 - self.scale + + let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) + let count = 12 + let relativeDashLength: CGFloat = 0.25 + let dashLength = perimeter / CGFloat(count) + self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] + + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath + } +} + +private class DrawingTextLayoutManager: NSLayoutManager { + var radius: CGFloat + var maxIndex: Int = 0 + + private(set) var path: UIBezierPath? + var rectArray: [CGRect] = [] + + var strokeColor: UIColor? + var strokeWidth: CGFloat = 0.0 + var strokeOffset: CGPoint = .zero + + var frameColor: UIColor? + var frameWidthInset: CGFloat = 0.0 + + override init() { + self.radius = 8.0 + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func prepare() { + self.path = nil + self.rectArray.removeAll() + + self.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: ((self.textStorage?.string ?? "") as NSString).length)) { rect, usedRect, textContainer, glyphRange, _ in + var ignoreRange = false + let charecterRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) + let substring = ((self.textStorage?.string ?? "") as NSString).substring(with: charecterRange) + if substring.trimmingCharacters(in: .newlines).isEmpty { + ignoreRange = true + } + + if !ignoreRange { + let newRect = CGRect(origin: CGPoint(x: usedRect.minX - self.frameWidthInset, y: usedRect.minY), size: CGSize(width: usedRect.width + self.frameWidthInset * 2.0, height: usedRect.height)) + self.rectArray.append(newRect) + } + } + + self.preprocess() + } + + private func preprocess() { + self.maxIndex = 0 + if self.rectArray.count < 2 { + return + } + for i in 1 ..< self.rectArray.count { + self.maxIndex = i + self.processRectIndex(i) + } + } + + private func processRectIndex(_ index: Int) { + guard self.rectArray.count >= 2 && index > 0 && index <= self.maxIndex else { + return + } + + let last = self.rectArray[index - 1] + let cur = self.rectArray[index] + + self.radius = cur.height * 0.18 + + let t1 = ((cur.minX - last.minX < 2.0 * self.radius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -2.0 * self.radius) && (cur.maxX < last.maxX)) + let t2 = ((last.minX - cur.minX < 2.0 * self.radius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -2.0 * self.radius) && (last.maxX < cur.maxX)) + + if t2 { + let newRect = CGRect(origin: CGPoint(x: cur.minX, y: last.minY), size: CGSize(width: cur.width, height: last.height)) + self.rectArray[index - 1] = newRect + self.processRectIndex(index - 1) + } + if t1 { + let newRect = CGRect(origin: CGPoint(x: last.minX, y: cur.minY), size: CGSize(width: last.width, height: cur.height)) + self.rectArray[index] = newRect + self.processRectIndex(index + 1) + } + } + + override func showCGGlyphs(_ glyphs: UnsafePointer, positions: UnsafePointer, count glyphCount: Int, font: UIFont, matrix textMatrix: CGAffineTransform, attributes: [NSAttributedString.Key : Any] = [:], in graphicsContext: CGContext) { + if let strokeColor = self.strokeColor { + graphicsContext.setStrokeColor(strokeColor.cgColor) + graphicsContext.setLineJoin(.round) + + let lineWidth = self.strokeWidth > 0.0 ? self.strokeWidth : font.pointSize / 9.0 + graphicsContext.setLineWidth(lineWidth) + graphicsContext.setTextDrawingMode(.stroke) + + graphicsContext.saveGState() + graphicsContext.translateBy(x: self.strokeOffset.x, y: self.strokeOffset.y) + + super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext) + + graphicsContext.restoreGState() + + let textColor: UIColor = attributes[NSAttributedString.Key.foregroundColor] as? UIColor ?? UIColor.white + + graphicsContext.setFillColor(textColor.cgColor) + graphicsContext.setTextDrawingMode(.fill) + } + super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, matrix: textMatrix, attributes: attributes, in: graphicsContext) + } + + override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { + if let frameColor = self.frameColor, let context = UIGraphicsGetCurrentContext() { + context.saveGState() + + context.translateBy(x: origin.x, y: origin.y) + + context.setBlendMode(.copy) + context.setFillColor(frameColor.cgColor) + context.setStrokeColor(frameColor.cgColor) + + self.prepare() + self.preprocess() + + let path = UIBezierPath() + + var last: CGRect = .null + for i in 0 ..< self.rectArray.count { + let cur = self.rectArray[i] + self.radius = cur.height * 0.18 + + path.append(UIBezierPath(roundedRect: cur, cornerRadius: self.radius)) + if i == 0 { + last = cur + } else if i > 0 && abs(last.maxY - cur.minY) < 10.0 { + let a = cur.origin + let b = CGPoint(x: cur.maxX, y: cur.minY) + let c = CGPoint(x: last.minX, y: last.maxY) + let d = CGPoint(x: last.maxX, y: last.maxY) + + if a.x - c.x >= 2.0 * self.radius { + let addPath = UIBezierPath(arcCenter: CGPoint(x: a.x - self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: 0.0, clockwise: true) + addPath.append( + UIBezierPath(arcCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: 3.0 * .pi * 0.5, clockwise: true) + ) + addPath.addLine(to: CGPoint(x: a.x - self.radius, y: a.y)) + path.append(addPath) + } + if a.x == c.x { + path.move(to: CGPoint(x: a.x, y: a.y - self.radius)) + path.addLine(to: CGPoint(x: a.x, y: a.y + self.radius)) + path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y + self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5 * 3.0, clockwise: true) + path.addArc(withCenter: CGPoint(x: a.x + self.radius, y: a.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true) + } + if d.x - b.x >= 2.0 * self.radius { + let addPath = UIBezierPath(arcCenter: CGPoint(x: b.x + self.radius, y: b.y + self.radius), radius: self.radius, startAngle: .pi * 0.5 * 3.0, endAngle: .pi, clockwise: false) + addPath.append( + UIBezierPath(arcCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false) + ) + addPath.addLine(to: CGPoint(x: b.x + self.radius, y: b.y)) + path.append(addPath) + } + if d.x == b.x { + path.move(to: CGPoint(x: b.x, y: b.y - self.radius)) + path.addLine(to: CGPoint(x: b.x, y: b.y + self.radius)) + path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y + self.radius), radius: self.radius, startAngle: 0.0, endAngle: 3.0 * .pi * 0.5, clockwise: false) + path.addArc(withCenter: CGPoint(x: b.x - self.radius, y: b.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false) + } + if c.x - a.x >= 2.0 * self.radius { + let addPath = UIBezierPath(arcCenter: CGPoint(x: c.x - self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: 0.0, clockwise: false) + addPath.append( + UIBezierPath(arcCenter: CGPoint(x: c.x + self.radius, y: c.y - self.radius), radius: self.radius, startAngle: .pi, endAngle: .pi * 0.5, clockwise: false) + ) + addPath.addLine(to: CGPoint(x: c.x - self.radius, y: c.y)) + path.append(addPath) + } + if b.x - d.x >= 2.0 * self.radius { + let addPath = UIBezierPath(arcCenter: CGPoint(x: d.x + self.radius, y: d.y - self.radius), radius: self.radius, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true) + addPath.append( + UIBezierPath(arcCenter: CGPoint(x: d.x - self.radius, y: d.y - self.radius), radius: self.radius, startAngle: 0.0, endAngle: .pi * 0.5, clockwise: true) + ) + addPath.addLine(to: CGPoint(x: d.x + self.radius, y: d.y)) + path.append(addPath) + } + + last = cur + } + } + self.path = path + + self.path?.fill() + self.path?.stroke() + + context.restoreGState() + } + } +} + +private class DrawingTextStorage: NSTextStorage { + let impl: NSTextStorage + + override init() { + self.impl = NSTextStorage() + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var string: String { + return self.impl.string + } + + override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] { + return self.impl.attributes(at: location, effectiveRange: range) + } + + override func replaceCharacters(in range: NSRange, with str: String) { + self.beginEditing() + self.impl.replaceCharacters(in: range, with: str) + self.edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length) + self.endEditing() + } + + override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) { + self.beginEditing() + self.impl.setAttributes(attrs, range: range) + self.edited(.editedAttributes, range: range, changeInLength: 0) + self.endEditing() + } +} + +class DrawingTextView: UITextView { + fileprivate var drawingLayoutManager: DrawingTextLayoutManager { + return self.layoutManager as! DrawingTextLayoutManager + } + + var strokeColor: UIColor? { + didSet { + self.drawingLayoutManager.strokeColor = self.strokeColor + self.setNeedsDisplay() + } + } + var strokeWidth: CGFloat = 0.0 { + didSet { + self.drawingLayoutManager.strokeWidth = self.strokeWidth + self.setNeedsDisplay() + } + } + var strokeOffset: CGPoint = .zero { + didSet { + self.drawingLayoutManager.strokeOffset = self.strokeOffset + self.setNeedsDisplay() + } + } + var frameColor: UIColor? { + didSet { + self.drawingLayoutManager.frameColor = self.frameColor + self.setNeedsDisplay() + } + } + var frameWidthInset: CGFloat = 0.0 { + didSet { + self.drawingLayoutManager.frameWidthInset = self.frameWidthInset + self.setNeedsDisplay() + } + } + + override var textColor: UIColor? { + get { + return super.textColor + } + set { + super.textColor = newValue + self.fixTypingAttributes() + } + } + + init(frame: CGRect) { + let textStorage = DrawingTextStorage() + let layoutManager = DrawingTextLayoutManager() + + let textContainer = NSTextContainer(size: CGSize(width: 0.0, height: .greatestFiniteMagnitude)) + textContainer.widthTracksTextView = true + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + super.init(frame: frame, textContainer: textContainer) + + self.tintColor = UIColor.white + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func caretRect(for position: UITextPosition) -> CGRect { + var rect = super.caretRect(for: position) + rect.size.width = floorToScreenPixels(rect.size.height / 25.0) + return rect + } + + override func insertText(_ text: String) { + self.fixTypingAttributes() + super.insertText(text) + self.fixTypingAttributes() + } + + override func paste(_ sender: Any?) { + self.fixTypingAttributes() + super.paste(sender) + self.fixTypingAttributes() + } + + fileprivate func fixTypingAttributes() { + var attributes: [NSAttributedString.Key: Any] = [:] + if let font = self.font { + attributes[NSAttributedString.Key.font] = font + } + if let textColor = self.textColor { + attributes[NSAttributedString.Key.foregroundColor] = textColor + } + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = self.textAlignment + attributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle + self.typingAttributes = attributes + } +} + +private var availableFonts: [String: (String, String)] = { + let familyNames = UIFont.familyNames + var result: [String: (String, String)] = [:] + + for family in familyNames { + let names = UIFont.fontNames(forFamilyName: family) + + var preferredFont: String? + for name in names { + let originalName = name + let name = name.lowercased() + if (!name.contains("-") || name.contains("regular")) && preferredFont == nil { + preferredFont = originalName + } + if name.contains("bold") && !name.contains("italic") { + preferredFont = originalName + } + } + + if let preferredFont { + let shortname = family.lowercased().replacingOccurrences(of: " ", with: "", options: []) + result[shortname] = (preferredFont, family) + } + } + return result +}() diff --git a/submodules/DrawingUI/Sources/DrawingTools.swift b/submodules/DrawingUI/Sources/DrawingTools.swift new file mode 100644 index 00000000000..078ad69a155 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingTools.swift @@ -0,0 +1,150 @@ +import Foundation +import UIKit +import Display + +final class MarkerTool: DrawingElement { + let uuid: UUID + + let drawingSize: CGSize + let color: DrawingColor + + let renderLineWidth: CGFloat + + var translation = CGPoint() + + var points: [CGPoint] = [] + + weak var metalView: DrawingMetalView? + + var isValid: Bool { + return self.points.count > 6 + } + + var bounds: CGRect { + var minX: CGFloat = .greatestFiniteMagnitude + var minY: CGFloat = .greatestFiniteMagnitude + var maxX: CGFloat = 0.0 + var maxY: CGFloat = 0.0 + + for point in self.points { + if point.x < minX { + minX = point.x + } + if point.x > maxX { + maxX = point.x + } + if point.y < minY { + minY = point.y + } + if point.y > maxY { + maxY = point.y + } + } + + return normalizeDrawingRect(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY).insetBy(dx: -80.0, dy: -80.0), drawingSize: self.drawingSize) + } + + required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.drawingSize = drawingSize + self.color = color + + let minLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.01) + let maxLineWidth = max(20.0, max(drawingSize.width, drawingSize.height) * 0.09) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth + + self.renderLineWidth = lineWidth + } + + func setupRenderView(screenSize: CGSize) -> DrawingRenderView? { + return nil + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + private var didSetup = false + func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) { + let filterDistance: CGFloat = 10.0 / zoomScale + if let lastPoint = self.points.last, lastPoint.distance(to: point.location) < filterDistance { + } else { + self.points.append(point.location) + } + + self.didSetup = true + self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth) + } + + func draw(in context: CGContext, size: CGSize) { + guard !self.points.isEmpty else { + return + } + context.saveGState() + + context.translateBy(x: self.translation.x, y: self.translation.y) + + self.metalView?.drawInContext(context) + self.metalView?.clear() + + context.restoreGState() + } +} + +final class FillTool: DrawingElement { + let uuid: UUID + + let drawingSize: CGSize + let color: DrawingColor + let isBlur: Bool + var blurredImage: UIImage? + + var translation = CGPoint() + + var isValid: Bool { + return true + } + + var bounds: CGRect { + return CGRect(origin: .zero, size: self.drawingSize) + } + + required init(drawingSize: CGSize, color: DrawingColor, blur: Bool, blurredImage: UIImage?) { + self.uuid = UUID() + self.drawingSize = drawingSize + self.color = color + self.isBlur = blur + self.blurredImage = blurredImage + } + + func setupRenderView(screenSize: CGSize) -> DrawingRenderView? { + return nil + } + + func setupRenderLayer() -> DrawingRenderLayer? { + return nil + } + + func updatePath(_ path: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) { + } + + func draw(in context: CGContext, size: CGSize) { + context.setShouldAntialias(false) + + context.setBlendMode(.copy) + + if self.isBlur { + if let blurredImage = self.blurredImage?.cgImage { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(blurredImage, in: CGRect(origin: .zero, size: size)) + } + } else { + context.setFillColor(self.color.toCGColor()) + context.fill(CGRect(origin: .zero, size: size)) + } + + context.setBlendMode(.normal) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingUtils.swift b/submodules/DrawingUI/Sources/DrawingUtils.swift new file mode 100644 index 00000000000..3570f6cbea5 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingUtils.swift @@ -0,0 +1,676 @@ +import Foundation +import UIKit +import QuartzCore +import simd + +public struct DrawingColor: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case red + case green + case blue + case alpha + case position + } + + public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + + public var red: CGFloat + public var green: CGFloat + public var blue: CGFloat + public var alpha: CGFloat + + public var position: CGPoint? + + var isClear: Bool { + return self.red.isZero && self.green.isZero && self.blue.isZero && self.alpha.isZero + } + + public init( + red: CGFloat, + green: CGFloat, + blue: CGFloat, + alpha: CGFloat = 1.0, + position: CGPoint? = nil + ) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + self.position = position + } + + public init(color: UIColor) { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 1.0 + if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + self.init(red: red, green: green, blue: blue, alpha: alpha) + } else if color.getWhite(&red, alpha: &alpha) { + self.init(red: red, green: red, blue: red, alpha: alpha) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0) + } + } + + public init(rgb: UInt32) { + self.init(color: UIColor(rgb: rgb)) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.red = try container.decode(CGFloat.self, forKey: .red) + self.green = try container.decode(CGFloat.self, forKey: .green) + self.blue = try container.decode(CGFloat.self, forKey: .blue) + self.alpha = try container.decode(CGFloat.self, forKey: .alpha) + self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.red, forKey: .red) + try container.encode(self.green, forKey: .green) + try container.encode(self.blue, forKey: .blue) + try container.encode(self.alpha, forKey: .alpha) + try container.encodeIfPresent(self.position, forKey: .position) + } + + func withUpdatedRed(_ red: CGFloat) -> DrawingColor { + return DrawingColor( + red: red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + func withUpdatedGreen(_ green: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: green, + blue: self.blue, + alpha: self.alpha + ) + } + + func withUpdatedBlue(_ blue: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: blue, + alpha: self.alpha + ) + } + + func withUpdatedAlpha(_ alpha: CGFloat) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: alpha, + position: self.position + ) + } + + func withUpdatedPosition(_ position: CGPoint) -> DrawingColor { + return DrawingColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + position: position + ) + } + + func toUIColor() -> UIColor { + return UIColor( + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha + ) + } + + func toCGColor() -> CGColor { + return self.toUIColor().cgColor + } + + func toFloat4() -> vector_float4 { + return [ + simd_float1(self.red), + simd_float1(self.green), + simd_float1(self.blue), + simd_float1(self.alpha) + ] + } + + public static func ==(lhs: DrawingColor, rhs: DrawingColor) -> Bool { + if lhs.red != rhs.red { + return false + } + if lhs.green != rhs.green { + return false + } + if lhs.blue != rhs.blue { + return false + } + if lhs.alpha != rhs.alpha { + return false + } + return true + } +} + +extension UIBezierPath { + convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) { + self.init() + + let path = CGMutablePath() + + let topLeft = rect.origin + let topRight = CGPoint(x: rect.maxX, y: rect.minY) + let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) + let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) + + if topLeftRadius != .zero { + path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.move(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + if topRightRadius != .zero { + path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y)) + path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius)) + } else { + path.addLine(to: CGPoint(x: topRight.x, y: topRight.y)) + } + + if bottomRightRadius != .zero { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius)) + path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y)) + } else { + path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y)) + } + + if bottomLeftRadius != .zero { + path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y)) + path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius)) + } else { + path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y)) + } + + if topLeftRadius != .zero { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius)) + path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y)) + } else { + path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y)) + } + + path.closeSubpath() + self.cgPath = path + } +} + +extension CGPoint { + func isEqual(to point: CGPoint, epsilon: CGFloat) -> Bool { + if x - epsilon <= point.x && point.x <= x + epsilon && y - epsilon <= point.y && point.y <= y + epsilon { + return true + } + return false + } + + static public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + static public func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + static public func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) + } + + static public func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) + } + + var length: CGFloat { + return sqrt(self.x * self.x + self.y * self.y) + } + + static func middle(p1: CGPoint, p2: CGPoint) -> CGPoint { + return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5) + } + + func distance(to point: CGPoint) -> CGFloat { + return sqrt(pow((point.x - self.x), 2) + pow((point.y - self.y), 2)) + } + + func distanceSquared(to point: CGPoint) -> CGFloat { + return pow((point.x - self.x), 2) + pow((point.y - self.y), 2) + } + + func angle(to point: CGPoint) -> CGFloat { + return atan2((point.y - self.y), (point.x - self.x)) + } + + func pointAt(distance: CGFloat, angle: CGFloat) -> CGPoint { + return CGPoint(x: distance * cos(angle) + self.x, y: distance * sin(angle) + self.y) + } + + func point(to point: CGPoint, t: CGFloat) -> CGPoint { + return CGPoint(x: self.x + t * (point.x - self.x), y: self.y + t * (point.y - self.y)) + } + + func perpendicularPointOnLine(start: CGPoint, end: CGPoint) -> CGPoint { + let l2 = start.distanceSquared(to: end) + if l2.isZero { + return start + } + let t = ((self.x - start.x) * (end.x - start.x) + (self.y - start.y) * (end.y - start.y)) / l2 + return CGPoint(x: start.x + t * (end.x - start.x), y: start.y + t * (end.y - start.y)) + } + + func linearBezierPoint(to: CGPoint, t: CGFloat) -> CGPoint { + let dx = to.x - x; + let dy = to.y - y; + + let px = x + (t * dx); + let py = y + (t * dy); + + return CGPoint(x: px, y: py) + } + + fileprivate func _cubicBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat { + let _t = 1 - t; + let _t2 = _t * _t; + let _t3 = _t * _t * _t ; + let t2 = t * t; + let t3 = t * t * t; + + return _t3 * start + + 3.0 * _t2 * t * c1 + + 3.0 * _t * t2 * c2 + + t3 * end; + } + + func cubicBezierPoint(to: CGPoint, controlPoint1 c1: CGPoint, controlPoint2 c2: CGPoint, t: CGFloat) -> CGPoint { + let x = _cubicBezier(t, self.x, c1.x, c2.x, to.x); + let y = _cubicBezier(t, self.y, c1.y, c2.y, to.y); + + return CGPoint(x: x, y: y); + } + + fileprivate func _quadBezier(_ t: CGFloat, _ start: CGFloat, _ c1: CGFloat, _ end: CGFloat) -> CGFloat { + let _t = 1 - t; + let _t2 = _t * _t; + let t2 = t * t; + + return _t2 * start + + 2 * _t * t * c1 + + t2 * end; + } + + func quadBezierPoint(to: CGPoint, controlPoint: CGPoint, t: CGFloat) -> CGPoint { + let x = _quadBezier(t, self.x, controlPoint.x, to.x); + let y = _quadBezier(t, self.y, controlPoint.y, to.y); + + return CGPoint(x: x, y: y); + } +} + + +extension CGPath { + static func star(in rect: CGRect, extrusion: CGFloat, points: Int = 5) -> CGPath { + func pointFrom(angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint { + return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y) + } + + let path = CGMutablePath() + + let center = rect.center.offsetBy(dx: 0.0, dy: rect.height * 0.05) + var angle: CGFloat = -CGFloat(.pi / 2.0) + let angleIncrement = CGFloat(.pi * 2.0 / Double(points)) + let radius = rect.width / 2.0 + + var firstPoint = true + for _ in 1 ... points { + let point = center.pointAt(distance: radius, angle: angle) + let nextPoint = center.pointAt(distance: radius, angle: angle + angleIncrement) + let midPoint = center.pointAt(distance: extrusion, angle: angle + angleIncrement * 0.5) + + if firstPoint { + firstPoint = false + path.move(to: point) + } + path.addLine(to: midPoint) + path.addLine(to: nextPoint) + + angle += angleIncrement + } + path.closeSubpath() + + return path + } + + static func arrow(from point: CGPoint, controlPoint: CGPoint, width: CGFloat, height: CGFloat, isOpen: Bool) -> CGPath { + let angle = atan2(point.y - controlPoint.y, point.x - controlPoint.x) + let angleAdjustment = atan2(width, -height) + let distance = hypot(width, height) + + let path = CGMutablePath() + path.move(to: point) + path.addLine(to: point.pointAt(distance: distance, angle: angle - angleAdjustment)) + if isOpen { + path.addLine(to: point) + } + path.addLine(to: point.pointAt(distance: distance, angle: angle + angleAdjustment)) + if isOpen { + path.addLine(to: point) + } else { + path.closeSubpath() + } + return path + } + + static func curve(start: CGPoint, end: CGPoint, mid: CGPoint, lineWidth: CGFloat?, arrowSize: CGSize?, twoSided: Bool = false) -> CGPath { + let linePath = CGMutablePath() + + let controlPoints = configureControlPoints(data: [start, mid, end]) + var lineStart = start + if let arrowSize = arrowSize, twoSided { + lineStart = start.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[0].ctrl1.angle(to: start)) + } + linePath.move(to: lineStart) + linePath.addCurve(to: mid, control1: controlPoints[0].ctrl1, control2: controlPoints[0].ctrl2) + + var lineEnd = end + if let arrowSize = arrowSize { + lineEnd = end.pointAt(distance: -arrowSize.height * 0.5, angle: controlPoints[1].ctrl1.angle(to: end)) + } + linePath.addCurve(to: lineEnd, control1: controlPoints[1].ctrl1, control2: controlPoints[1].ctrl2) + + let path: CGMutablePath + if let lineWidth = lineWidth, let mutablePath = linePath.copy(strokingWithWidth: lineWidth, lineCap: .square, lineJoin: .round, miterLimit: 0.0).mutableCopy() { + path = mutablePath + } else { + path = linePath + } + + if let arrowSize = arrowSize { + let arrowPath = arrow(from: end, controlPoint: controlPoints[1].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false) + path.addPath(arrowPath) + + if twoSided { + let secondArrowPath = arrow(from: start, controlPoint: controlPoints[0].ctrl1, width: arrowSize.width, height: arrowSize.height, isOpen: false) + path.addPath(secondArrowPath) + } + } + + return path + } + + static func bubble(in rect: CGRect, cornerRadius: CGFloat, smallCornerRadius: CGFloat, tailPosition: CGPoint, tailWidth: CGFloat) -> CGPath { + let r1 = min(cornerRadius, min(rect.width, rect.height) / 3.0) + let r2 = min(smallCornerRadius, min(rect.width, rect.height) / 10.0) + + let ax = tailPosition.x * rect.width + let ay = tailPosition.y + + let width = min(max(tailWidth, ay / 2.0), rect.width / 4.0) + let angle = atan2(ay, width) + let h = r2 / tan(angle / 2.0) + + let r1a = min(r1, min(rect.maxX - ax, ax - rect.minX) * 0.5) + let r2a = min(r2, min(rect.maxX - ax, ax - rect.minX) * 0.2) + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.minY + r1), radius: r1, startAngle: .pi, endAngle: .pi * 3.0 / 2.0, clockwise: false) + path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.minY + r1), radius: r1, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false) + + if ax > rect.width / 2.0 { + if ax < rect.width - 1 { + path.addArc(center: CGPoint(x: rect.maxX - r1a, y: rect.maxY - r1a), radius: r1a, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false) + path.addArc(center: CGPoint(x: rect.minX + ax + r2a, y: rect.maxY + r2a), radius: r2a, startAngle: .pi * 3.0 / 2.0, endAngle: .pi, clockwise: true) + } + path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay)) + path.addArc(center: CGPoint(x: rect.minX + ax - width - r2, y: rect.maxY + h), radius: h, startAngle: -(CGFloat.pi / 2 - angle), endAngle: CGFloat.pi * 3 / 2, clockwise: true) + path.addArc(center: CGPoint(x: rect.minX + r1, y: rect.maxY - r1), radius: r1, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false) + } else { + path.addArc(center: CGPoint(x: rect.maxX - r1, y: rect.maxY - r1), radius: r1, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + path.addArc(center: CGPoint(x: rect.minX + ax + width + r2, y: rect.maxY + h), radius: h, startAngle: CGFloat.pi * 3 / 2, endAngle: CGFloat.pi * 3 / 2 - angle, clockwise: true) + path.addLine(to: CGPoint(x: rect.minX + ax, y: rect.maxY + ay)) + if ax > 1 { + path.addArc(center: CGPoint(x: rect.minX + ax - r2a, y: rect.maxY + r2a), radius: r2a, startAngle: 0, endAngle: CGFloat.pi * 3 / 2, clockwise: true) + path.addArc(center: CGPoint(x: rect.minX + r1a, y: rect.maxY - r1a), radius: r1a, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false) + } + } + + path.closeSubpath() + + return path + } +} + +private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2: CGPoint)] { + let segments = data.count - 1 + + if segments == 1 { + let p0 = data[0] + let p3 = data[1] + + return [(p0, p3)] + } else if segments > 1 { + var ad: [CGFloat] = [] + var d: [CGFloat] = [] + var bd: [CGFloat] = [] + + var rhsArray: [CGPoint] = [] + + for i in 0 ..< segments { + var rhsXValue: CGFloat = 0.0 + var rhsYValue: CGFloat = 0.0 + + let p0 = data[i] + let p3 = data[i + 1] + + if i == 0 { + bd.append(0.0) + d.append(2.0) + ad.append(1.0) + + rhsXValue = p0.x + 2.0 * p3.x + rhsYValue = p0.y + 2.0 * p3.y + } else if i == segments - 1 { + bd.append(2.0) + d.append(7.0) + ad.append(0.0) + + rhsXValue = 8.0 * p0.x + p3.x + rhsYValue = 8.0 * p0.y + p3.y + } else { + bd.append(1.0) + d.append(4.0) + ad.append(1.0) + + rhsXValue = 4.0 * p0.x + 2.0 * p3.x + rhsYValue = 4.0 * p0.y + 2.0 * p3.y + } + + rhsArray.append(CGPoint(x: rhsXValue, y: rhsYValue)) + } + + var firstControlPoints: [CGPoint?] = [] + var secondControlPoints: [CGPoint?] = [] + + var controlPoints : [(CGPoint, CGPoint)] = [] + + var solutionSet1 = [CGPoint?]() + solutionSet1 = Array(repeating: nil, count: segments) + + ad[0] = ad[0] / d[0] + rhsArray[0].x = rhsArray[0].x / d[0] + rhsArray[0].y = rhsArray[0].y / d[0] + + if segments > 2 { + for i in 1...segments - 2 { + let rhsValueX = rhsArray[i].x + let prevRhsValueX = rhsArray[i - 1].x + + let rhsValueY = rhsArray[i].y + let prevRhsValueY = rhsArray[i - 1].y + + ad[i] = ad[i] / (d[i] - bd[i] * ad[i - 1]); + + let exp1x = (rhsValueX - (bd[i] * prevRhsValueX)) + let exp1y = (rhsValueY - (bd[i] * prevRhsValueY)) + let exp2 = (d[i] - bd[i] * ad[i - 1]) + + rhsArray[i].x = exp1x / exp2 + rhsArray[i].y = exp1y / exp2 + } + } + + let lastElementIndex = segments - 1 + let exp1 = (rhsArray[lastElementIndex].x - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].x) + let exp1y = (rhsArray[lastElementIndex].y - bd[lastElementIndex] * rhsArray[lastElementIndex - 1].y) + let exp2 = (d[lastElementIndex] - bd[lastElementIndex] * ad[lastElementIndex - 1]) + rhsArray[lastElementIndex].x = exp1 / exp2 + rhsArray[lastElementIndex].y = exp1y / exp2 + + solutionSet1[lastElementIndex] = rhsArray[lastElementIndex] + + for i in (0.. Matrix { + m[12] = x + m[13] = y + m[14] = z + return self + } + + @discardableResult + func scaling(x: Float, y: Float, z: Float) -> Matrix { + m[0] = x + m[5] = y + m[10] = z + return self + } + + static var identity = Matrix() +} + +struct Vertex { + var position: vector_float4 + var texCoord: vector_float2 + + init(position: CGPoint, texCoord: CGPoint) { + self.position = position.toFloat4() + self.texCoord = texCoord.toFloat2() + } +} + +struct Point { + var position: vector_float4 + var color: vector_float4 + var angle: Float + var size: Float + + init(x: CGFloat, y: CGFloat, color: DrawingColor, size: CGFloat, angle: CGFloat = 0) { + self.position = vector_float4(Float(x), Float(y), 0, 1) + self.size = Float(size) + self.color = color.toFloat4() + self.angle = Float(angle) + } +} + +extension CGPoint { + func toFloat4(z: CGFloat = 0, w: CGFloat = 1) -> vector_float4 { + return [Float(x), Float(y), Float(z) ,Float(w)] + } + + func toFloat2() -> vector_float2 { + return [Float(x), Float(y)] + } + + func offsetBy(_ offset: CGPoint) -> CGPoint { + return self.offsetBy(dx: offset.x, dy: offset.y) + } +} + +func normalizeDrawingRect(_ rect: CGRect, drawingSize: CGSize) -> CGRect { + var rect = rect + if rect.origin.x < 0.0 { + rect.size.width += rect.origin.x + rect.origin.x = 0.0 + } + if rect.origin.y < 0.0 { + rect.size.height += rect.origin.y + rect.origin.y = 0.0 + } + if rect.maxX > drawingSize.width { + rect.size.width -= (rect.maxX - drawingSize.width) + } + if rect.maxY > drawingSize.height { + rect.size.height -= (rect.maxY - drawingSize.height) + } + return rect +} diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift new file mode 100644 index 00000000000..226f2b9d113 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -0,0 +1,403 @@ +import Foundation +import UIKit +import Display +import AccountContext + +public final class DrawingVectorEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case type + case color + case lineWidth + case drawingSize + case referenceDrawingSize + case start + case mid + case end + case renderImage + } + + public enum VectorType: Codable { + case line + case oneSidedArrow + case twoSidedArrow + } + + public let uuid: UUID + public let isAnimated: Bool + + var type: VectorType + public var color: DrawingColor + public var lineWidth: CGFloat + + public var drawingSize: CGSize + var referenceDrawingSize: CGSize + var start: CGPoint + var mid: (CGFloat, CGFloat) + var end: CGPoint + + var _cachedMidPoint: (start: CGPoint, end: CGPoint, midLength: CGFloat, midHeight: CGFloat, midPoint: CGPoint)? + var midPoint: CGPoint { + if let (start, end, midLength, midHeight, midPoint) = self._cachedMidPoint, start == self.start, end == self.end, midLength == self.mid.0, midHeight == self.mid.1 { + return midPoint + } else { + let midPoint = midPointPositionFor(start: self.start, end: self.end, length: self.mid.0, height: self.mid.1) + self._cachedMidPoint = (self.start, self.end, self.mid.0, self.mid.1, midPoint) + return midPoint + } + } + + public var center: CGPoint { + return self.start + } + + public var scale: CGFloat = 1.0 + + public var renderImage: UIImage? + + init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) { + self.uuid = UUID() + self.isAnimated = false + + self.type = type + self.color = color + self.lineWidth = lineWidth + + self.drawingSize = .zero + self.referenceDrawingSize = .zero + self.start = CGPoint() + self.mid = (0.5, 0.0) + self.end = CGPoint() + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.isAnimated = false + self.type = try container.decode(VectorType.self, forKey: .type) + self.color = try container.decode(DrawingColor.self, forKey: .color) + self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth) + self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize) + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.start = try container.decode(CGPoint.self, forKey: .start) + let mid = try container.decode(CGPoint.self, forKey: .mid) + self.mid = (mid.x, mid.y) + self.end = try container.decode(CGPoint.self, forKey: .end) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.type, forKey: .type) + try container.encode(self.color, forKey: .color) + try container.encode(self.lineWidth, forKey: .lineWidth) + try container.encode(self.drawingSize, forKey: .drawingSize) + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.start, forKey: .start) + try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid) + try container.encode(self.end, forKey: .end) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate() -> DrawingEntity { + let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth) + newEntity.drawingSize = self.drawingSize + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.start = self.start + newEntity.mid = self.mid + newEntity.end = self.end + return newEntity + } + + public weak var currentEntityView: DrawingEntityView? + public func makeView(context: AccountContext) -> DrawingEntityView { + let entityView = DrawingVectorEntityView(context: context, entity: self) + self.currentEntityView = entityView + return entityView + } + + public func prepareForRender() { + self.renderImage = (self.currentEntityView as? DrawingVectorEntityView)?.getRenderImage() + } +} + +final class DrawingVectorEntityView: DrawingEntityView { + private var vectorEntity: DrawingVectorEntity { + return self.entity as! DrawingVectorEntity + } + + fileprivate let shapeLayer = SimpleShapeLayer() + + init(context: AccountContext, entity: DrawingVectorEntity) { + super.init(context: context, entity: entity) + + self.layer.addSublayer(self.shapeLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var selectionBounds: CGRect { + return self.shapeLayer.path?.boundingBox ?? self.bounds + } + + private var maxLineWidth: CGFloat { + return max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.1) + } + + override func update(animated: Bool) { + self.center = CGPoint(x: self.vectorEntity.drawingSize.width * 0.5, y: self.vectorEntity.drawingSize.height * 0.5) + self.bounds = CGRect(origin: .zero, size: self.vectorEntity.drawingSize) + + let minLineWidth = max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.01) + let maxLineWidth = max(10.0, max(self.vectorEntity.referenceDrawingSize.width, self.vectorEntity.referenceDrawingSize.height) * 0.05) + let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * self.vectorEntity.lineWidth + + self.shapeLayer.path = CGPath.curve( + start: self.vectorEntity.start, + end: self.vectorEntity.end, + mid: self.vectorEntity.midPoint, + lineWidth: lineWidth, + arrowSize: self.vectorEntity.type == .line ? nil : CGSize(width: lineWidth * 1.5, height: lineWidth * 3.0), + twoSided: self.vectorEntity.type == .twoSidedArrow + ) + self.shapeLayer.fillColor = self.vectorEntity.color.toCGColor() + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingVectorEntititySelectionView else { + return + } + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + + let drawingSize = self.vectorEntity.drawingSize + selectionView.bounds = CGRect(origin: .zero, size: drawingSize) + selectionView.center = CGPoint(x: drawingSize.width * 0.5 * scale, y: drawingSize.height * 0.5 * scale) + selectionView.transform = CGAffineTransform(scaleX: scale, y: scale) + selectionView.scale = scale + } + + override func precisePoint(inside point: CGPoint) -> Bool { + if let path = self.shapeLayer.path { + if path.contains(point) { + return true + } else { + let expandedPath = CGPath.curve( + start: self.vectorEntity.start, + end: self.vectorEntity.end, + mid: self.vectorEntity.midPoint, + lineWidth: self.maxLineWidth * 0.8, + arrowSize: nil, + twoSided: false + ) + return expandedPath.contains(point) + } + } else { + return super.precisePoint(inside: point) + } + } + + override func makeSelectionView() -> DrawingEntitySelectionView { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingVectorEntititySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 1.0) + self.drawHierarchy(in: rect, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } +} + +private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, height: CGFloat) -> CGPoint { + let distance = end.distance(to: start) + let angle = start.angle(to: end) + let p1 = start.pointAt(distance: distance * length, angle: angle) + let p2 = p1.pointAt(distance: distance * height, angle: angle + .pi * 0.5) + return p2 +} + +final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGestureRecognizerDelegate { + private let startHandle = SimpleShapeLayer() + private let midHandle = SimpleShapeLayer() + private let endHandle = SimpleShapeLayer() + + private var panGestureRecognizer: UIPanGestureRecognizer! + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + self.startHandle.bounds = handleBounds + self.startHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + self.startHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.startHandle.rasterizationScale = UIScreen.main.scale + self.startHandle.shouldRasterize = true + + self.midHandle.bounds = handleBounds + self.midHandle.fillColor = UIColor(rgb: 0x00ff00).cgColor + self.midHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.midHandle.rasterizationScale = UIScreen.main.scale + self.midHandle.shouldRasterize = true + + self.endHandle.bounds = handleBounds + self.endHandle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + self.endHandle.strokeColor = UIColor(rgb: 0xffffff).cgColor + self.endHandle.rasterizationScale = UIScreen.main.scale + self.endHandle.shouldRasterize = true + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.layer.addSublayer(self.startHandle) + self.layer.addSublayer(self.midHandle) + self.layer.addSublayer(self.endHandle) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + self.panGestureRecognizer = panGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private var currentHandle: CALayer? + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingVectorEntity else { + return + } + let location = gestureRecognizer.location(in: self) + + switch gestureRecognizer.state { + case .began: + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + return + } + } + } + self.currentHandle = self.layer + case .changed: + let delta = gestureRecognizer.translation(in: entityView) + + var updatedStart = entity.start + var updatedMid = entity.mid + var updatedEnd = entity.end + + if self.currentHandle === self.startHandle { + updatedStart.x += delta.x + updatedStart.y += delta.y + } else if self.currentHandle === self.endHandle { + updatedEnd.x += delta.x + updatedEnd.y += delta.y + } else if self.currentHandle === self.midHandle { + var updatedMidPoint = entity.midPoint + updatedMidPoint.x += delta.x + updatedMidPoint.y += delta.y + + let distance = updatedStart.distance(to: updatedEnd) + let pointOnLine = updatedMidPoint.perpendicularPointOnLine(start: updatedStart, end: updatedEnd) + + let angle = updatedStart.angle(to: updatedEnd) + let midAngle = updatedStart.angle(to: updatedMidPoint) + var height = updatedMidPoint.distance(to: pointOnLine) / distance + var deltaAngle = midAngle - angle + if deltaAngle > .pi { + deltaAngle = angle - 2 * .pi + } else if deltaAngle < -.pi { + deltaAngle = angle + 2 * .pi + } + if deltaAngle < 0.0 { + height *= -1.0 + } + let length = updatedStart.distance(to: pointOnLine) / distance + updatedMid = (length, height) + } else if self.currentHandle === self.layer { + updatedStart.x += delta.x + updatedStart.y += delta.y + updatedEnd.x += delta.x + updatedEnd.y += delta.y + } + + entity.start = updatedStart + entity.mid = updatedMid + entity.end = updatedEnd + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended: + break + default: + break + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.startHandle.frame.contains(point) || self.midHandle.frame.contains(point) || self.endHandle.frame.contains(point) { + return true + } else if let entityView = self.entityView as? DrawingVectorEntityView, let path = entityView.shapeLayer.path { + return path.contains(self.convert(point, to: entityView)) + } + return false + } + + override func layoutSubviews() { + guard let entityView = self.entityView as? DrawingVectorEntityView, let entity = entityView.entity as? DrawingVectorEntity else { + return + } + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + self.startHandle.path = handlePath + self.startHandle.position = entity.start + self.startHandle.bounds = bounds + self.startHandle.lineWidth = lineWidth + + self.midHandle.path = handlePath + self.midHandle.position = entity.midPoint + self.midHandle.bounds = bounds + self.midHandle.lineWidth = lineWidth + + self.endHandle.path = handlePath + self.endHandle.position = entity.end + self.endHandle.bounds = bounds + self.endHandle.lineWidth = lineWidth + } + + var isTracking: Bool { + return gestureIsTracking(self.panGestureRecognizer) + } +} diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift new file mode 100644 index 00000000000..69f0f6893de --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -0,0 +1,1070 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import ComponentFlow +import LegacyComponents +import AppBundle +import ImageBlur + +protocol DrawingRenderLayer: CALayer { + +} + +protocol DrawingRenderView: UIView { + +} + +protocol DrawingElement: AnyObject { + var uuid: UUID { get } + var translation: CGPoint { get set } + var isValid: Bool { get } + var bounds: CGRect { get } + + func setupRenderView(screenSize: CGSize) -> DrawingRenderView? + func setupRenderLayer() -> DrawingRenderLayer? + func updatePath(_ point: DrawingPoint, state: DrawingGesturePipeline.DrawingGestureState, zoomScale: CGFloat) + + func draw(in: CGContext, size: CGSize) +} + +private enum DrawingOperation { + case clearAll(CGRect) + case slice(DrawingSlice) + case addEntity(UUID) + case removeEntity(DrawingEntity) +} + +public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInteractionDelegate, TGPhotoDrawingView { + public var zoomOut: () -> Void = {} + + struct NavigationState { + let canUndo: Bool + let canRedo: Bool + let canClear: Bool + let canZoomOut: Bool + let isDrawing: Bool + } + + enum Action { + case undo + case redo + case clear + case zoomOut + } + + enum Tool { + case pen + case arrow + case marker + case neon + case eraser + case blur + } + + var tool: Tool = .pen + var toolColor: DrawingColor = DrawingColor(color: .white) + var toolBrushSize: CGFloat = 0.25 + + var stateUpdated: (NavigationState) -> Void = { _ in } + + var shouldBegin: (CGPoint) -> Bool = { _ in return true } + var getFullImage: () -> UIImage? = { return nil } + + var requestedColorPicker: () -> Void = {} + var requestedEraserToggle: () -> Void = {} + var requestedToolsToggle: () -> Void = {} + + private var undoStack: [DrawingOperation] = [] + private var redoStack: [DrawingOperation] = [] + fileprivate var uncommitedElement: DrawingElement? + + private(set) var drawingImage: UIImage? + private let renderer: UIGraphicsImageRenderer + + private var currentDrawingViewContainer: UIImageView + private var currentDrawingRenderView: DrawingRenderView? + private var currentDrawingLayer: DrawingRenderLayer? + + private var metalView: DrawingMetalView? + + private let brushSizePreviewLayer: SimpleShapeLayer + + let imageSize: CGSize + private(set) var zoomScale: CGFloat = 1.0 + + private var drawingGesturePipeline: DrawingGesturePipeline? + private var longPressGestureRecognizer: UILongPressGestureRecognizer? + + private var loadedTemplates: [UnistrokeTemplate] = [] + private var previousStrokePoint: CGPoint? + private var strokeRecognitionTimer: SwiftSignalKit.Timer? + + private var isDrawing = false + private var drawingGestureStartTimestamp: Double? + + private func loadTemplates() { + func load(_ name: String) { + if let url = getAppBundle().url(forResource: name, withExtension: "json"), + let data = try? Data(contentsOf: url), + let json = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any], + let points = json["points"] as? [Any] + { + var strokePoints: [CGPoint] = [] + for point in points { + let x = (point as! [Any]).first as! Double + let y = (point as! [Any]).last as! Double + strokePoints.append(CGPoint(x: x, y: y)) + } + let template = UnistrokeTemplate(name: name, points: strokePoints) + self.loadedTemplates.append(template) + } + } + + load("shape_rectangle") + load("shape_circle") + load("shape_star") + load("shape_arrow") + } + + private let hapticFeedback = HapticFeedback() + + public var screenSize: CGSize + + private var previousPointTimestamp: Double? + + private let pencilInteraction: UIInteraction? + + init(size: CGSize) { + self.imageSize = size + self.screenSize = size + + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + if #available(iOS 12.0, *) { + format.preferredRange = .standard + } + format.opaque = false + self.renderer = UIGraphicsImageRenderer(size: size, format: format) + + self.currentDrawingViewContainer = UIImageView() + self.currentDrawingViewContainer.frame = CGRect(origin: .zero, size: size) + self.currentDrawingViewContainer.contentScaleFactor = 1.0 + self.currentDrawingViewContainer.backgroundColor = .clear + self.currentDrawingViewContainer.isUserInteractionEnabled = false + + self.brushSizePreviewLayer = SimpleShapeLayer() + self.brushSizePreviewLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)) + self.brushSizePreviewLayer.strokeColor = UIColor(rgb: 0x919191).cgColor + self.brushSizePreviewLayer.fillColor = UIColor.white.cgColor + self.brushSizePreviewLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0)), transform: nil) + self.brushSizePreviewLayer.opacity = 0.0 + self.brushSizePreviewLayer.shadowColor = UIColor.black.cgColor + self.brushSizePreviewLayer.shadowOpacity = 0.5 + self.brushSizePreviewLayer.shadowOffset = CGSize(width: 0.0, height: 3.0) + self.brushSizePreviewLayer.shadowRadius = 20.0 + + if #available(iOS 12.1, *) { + let pencilInteraction = UIPencilInteraction() + self.pencilInteraction = pencilInteraction + } else { + self.pencilInteraction = nil + } + + super.init(frame: CGRect(origin: .zero, size: size)) + + Queue.mainQueue().async { + self.loadTemplates() + } + + if #available(iOS 12.1, *), let pencilInteraction = self.pencilInteraction as? UIPencilInteraction { + pencilInteraction.delegate = self + self.addInteraction(pencilInteraction) + } + + self.backgroundColor = .clear + self.contentScaleFactor = 1.0 + self.isExclusiveTouch = true + + self.addSubview(self.currentDrawingViewContainer) + + self.layer.addSublayer(self.brushSizePreviewLayer) + + let drawingGesturePipeline = DrawingGesturePipeline(view: self) + drawingGesturePipeline.gestureRecognizer?.shouldBegin = { [weak self] point in + if let strongSelf = self { + if !strongSelf.shouldBegin(point) { + return false + } + if strongSelf.undoStack.isEmpty && !strongSelf.hasOpaqueData && strongSelf.tool == .eraser { + return false + } + if strongSelf.tool == .blur, strongSelf.preparedBlurredImage == nil { + return false + } + if let uncommitedElement = strongSelf.uncommitedElement as? PenTool, uncommitedElement.isFinishingArrow { + return false + } + return true + } else { + return false + } + } + drawingGesturePipeline.onDrawing = { [weak self] state, point in + guard let strongSelf = self else { + return + } + let currentTimestamp = CACurrentMediaTime() + switch state { + case .began: + strongSelf.isDrawing = true + strongSelf.previousStrokePoint = nil + strongSelf.drawingGestureStartTimestamp = currentTimestamp + strongSelf.previousPointTimestamp = currentTimestamp + + if strongSelf.uncommitedElement != nil { + strongSelf.finishDrawing(rect: CGRect(origin: .zero, size: strongSelf.imageSize), synchronous: true) + } + + if case .marker = strongSelf.tool, let metalView = strongSelf.metalView { + metalView.isHidden = false + } + + guard let newElement = strongSelf.setupNewElement() else { + return + } + + if let renderView = newElement.setupRenderView(screenSize: strongSelf.screenSize) { + if let currentDrawingView = strongSelf.currentDrawingRenderView { + strongSelf.currentDrawingRenderView = nil + currentDrawingView.removeFromSuperview() + } + if strongSelf.tool == .eraser { + strongSelf.currentDrawingViewContainer.removeFromSuperview() + strongSelf.currentDrawingViewContainer.backgroundColor = .white + + renderView.layer.compositingFilter = "xor" + + strongSelf.currentDrawingViewContainer.addSubview(renderView) + strongSelf.mask = strongSelf.currentDrawingViewContainer + } else if strongSelf.tool == .blur { + strongSelf.currentDrawingViewContainer.mask = renderView + strongSelf.currentDrawingViewContainer.image = strongSelf.preparedBlurredImage + } else { + strongSelf.currentDrawingViewContainer.addSubview(renderView) + } + strongSelf.currentDrawingRenderView = renderView + } + + if let renderLayer = newElement.setupRenderLayer() { + if let currentDrawingLayer = strongSelf.currentDrawingLayer { + strongSelf.currentDrawingLayer = nil + currentDrawingLayer.removeFromSuperlayer() + } + if strongSelf.tool == .eraser { + strongSelf.currentDrawingViewContainer.removeFromSuperview() + strongSelf.currentDrawingViewContainer.backgroundColor = .white + + renderLayer.compositingFilter = "xor" + + strongSelf.currentDrawingViewContainer.layer.addSublayer(renderLayer) + strongSelf.mask = strongSelf.currentDrawingViewContainer + } else if strongSelf.tool == .blur { + strongSelf.currentDrawingViewContainer.layer.mask = renderLayer + strongSelf.currentDrawingViewContainer.image = strongSelf.preparedBlurredImage + } else { + strongSelf.currentDrawingViewContainer.layer.addSublayer(renderLayer) + } + strongSelf.currentDrawingLayer = renderLayer + } + newElement.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) + strongSelf.uncommitedElement = newElement + strongSelf.updateInternalState() + case .changed: + if let previousPointTimestamp = strongSelf.previousPointTimestamp, currentTimestamp - previousPointTimestamp < 0.016 { + return + } + strongSelf.previousPointTimestamp = currentTimestamp + strongSelf.uncommitedElement?.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) + +// if case let .direct(point) = path, let lastPoint = line.points.last { +// if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 { +// let currentTimestamp = CACurrentMediaTime() +// if lastPoint.location.distance(to: previousStrokePoint) > 10.0 { +// strongSelf.previousStrokePoint = lastPoint.location +// +// strongSelf.strokeRecognitionTimer?.invalidate() +// strongSelf.strokeRecognitionTimer = nil +// } +// +// if strongSelf.strokeRecognitionTimer == nil, let startTimestamp = strongSelf.drawingGestureStartTimestamp, currentTimestamp - startTimestamp < 3.0 { +// strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in +// guard let strongSelf = self else { +// return +// } +// if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 { +// let strokeRecognizer = Unistroke(points: line.points.map { $0.location }) +// if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) { +// let edges = line.bounds +// let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY)) +// +// var entity: DrawingEntity? +// if template == "shape_rectangle" { +// let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) +// shapeEntity.referenceDrawingSize = strongSelf.imageSize +// shapeEntity.position = bounds.center +// shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1) +// entity = shapeEntity +// } else if template == "shape_circle" { +// let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) +// shapeEntity.referenceDrawingSize = strongSelf.imageSize +// shapeEntity.position = bounds.center +// shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1) +// entity = shapeEntity +// } else if template == "shape_star" { +// let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) +// shapeEntity.referenceDrawingSize = strongSelf.imageSize +// shapeEntity.position = bounds.center +// shapeEntity.size = CGSize(width: max(bounds.width, bounds.height) * 1.1, height: max(bounds.width, bounds.height) * 1.1) +// entity = shapeEntity +// } else if template == "shape_arrow" { +// let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize) +// arrowEntity.referenceDrawingSize = strongSelf.imageSize +// arrowEntity.start = line.points.first?.location ?? .zero +// arrowEntity.end = line.points[line.points.count - 4].location +// entity = arrowEntity +// } +// +// if let entity = entity { +// strongSelf.entitiesView?.add(entity) +// strongSelf.entitiesView?.selectEntity(entity) +// strongSelf.cancelDrawing() +// strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false +// strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true +// } +// } +// } +// strongSelf.strokeRecognitionTimer?.invalidate() +// strongSelf.strokeRecognitionTimer = nil +// }, queue: Queue.mainQueue()) +// strongSelf.strokeRecognitionTimer?.start() +// } +// } else { +// strongSelf.previousStrokePoint = lastPoint.location +// } +// } + case .ended, .cancelled: + strongSelf.isDrawing = false + strongSelf.strokeRecognitionTimer?.invalidate() + strongSelf.strokeRecognitionTimer = nil + strongSelf.uncommitedElement?.updatePath(point, state: state, zoomScale: strongSelf.zoomScale) + + if strongSelf.uncommitedElement?.isValid == true { + let bounds = strongSelf.uncommitedElement?.bounds + Queue.mainQueue().after(0.05) { + if let bounds = bounds { + strongSelf.finishDrawing(rect: bounds, synchronous: true) + } + } + } else { + strongSelf.cancelDrawing() + } + strongSelf.updateInternalState() + } + } + self.drawingGesturePipeline = drawingGesturePipeline + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + longPressGestureRecognizer.minimumPressDuration = 0.45 + longPressGestureRecognizer.allowableMovement = 2.0 + longPressGestureRecognizer.delegate = self + self.addGestureRecognizer(longPressGestureRecognizer) + self.longPressGestureRecognizer = longPressGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.longPressTimer?.invalidate() + self.strokeRecognitionTimer?.invalidate() + } + + public func setup(withDrawing drawingData: Data?) { + if let drawingData = drawingData, let image = UIImage(data: drawingData) { + self.hasOpaqueData = true + + if let context = DrawingContext(size: image.size, scale: 1.0, opaque: false) { + context.withFlippedContext { context in + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: image.size)) + } + } + self.drawingImage = context.generateImage() ?? image + } else { + self.drawingImage = image + } + self.layer.contents = image.cgImage + self.updateInternalState() + } + } + + var hasOpaqueData = false + var drawingData: Data? { + guard !self.undoStack.isEmpty || self.hasOpaqueData else { + return nil + } + return self.drawingImage?.pngData() + } + + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + @available(iOS 12.1, *) + public func pencilInteractionDidTap(_ interaction: UIPencilInteraction) { + switch UIPencilInteraction.preferredTapAction { + case .switchEraser: + self.requestedEraserToggle() + case .showColorPalette: + self.requestedColorPicker() + case .switchPrevious: + self.requestedToolsToggle() + default: + break + } + } + + private var longPressTimer: SwiftSignalKit.Timer? + private var fillCircleLayer: CALayer? + @objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let location = gestureRecognizer.location(in: self) + switch gestureRecognizer.state { + case .began: + self.longPressTimer?.invalidate() + self.longPressTimer = nil + + if self.longPressTimer == nil { + var toolColor = self.toolColor + var blurredImage: UIImage? + if self.tool == .marker { + toolColor = toolColor.withUpdatedAlpha(toolColor.alpha * 0.7) + } else if self.tool == .eraser { + toolColor = DrawingColor.clear + } else if self.tool == .blur { + blurredImage = self.preparedBlurredImage + } + + self.hapticFeedback.prepareImpact(.medium) + + let fillCircleLayer = SimpleShapeLayer() + self.longPressTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self, weak fillCircleLayer] in + if let strongSelf = self { + strongSelf.cancelDrawing() + + let action = { + let newElement = FillTool(drawingSize: strongSelf.imageSize, color: toolColor, blur: blurredImage != nil, blurredImage: blurredImage) + strongSelf.uncommitedElement = newElement + strongSelf.finishDrawing(rect: CGRect(origin: .zero, size: strongSelf.imageSize), synchronous: true) + } + if [.eraser, .blur].contains(strongSelf.tool) { + UIView.transition(with: strongSelf, duration: 0.2, options: .transitionCrossDissolve) { + action() + } + } else { + action() + } + + strongSelf.fillCircleLayer = nil + fillCircleLayer?.removeFromSuperlayer() + + strongSelf.hapticFeedback.impact(.medium) + } + }, queue: Queue.mainQueue()) + self.longPressTimer?.start() + + if [.eraser, .blur].contains(self.tool) { + + } else { + fillCircleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0)) + fillCircleLayer.position = location + fillCircleLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: CGSize(width: 160.0, height: 160.0))).cgPath + fillCircleLayer.fillColor = toolColor.toCGColor() + + self.layer.addSublayer(fillCircleLayer) + self.fillCircleLayer = fillCircleLayer + + fillCircleLayer.animateScale(from: 0.01, to: 12.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + if let fillCircleLayer = strongSelf.fillCircleLayer { + strongSelf.fillCircleLayer = nil + fillCircleLayer.removeFromSuperlayer() + } + } + }) + } + } + case .ended, .cancelled: + self.longPressTimer?.invalidate() + self.longPressTimer = nil + if let fillCircleLayer = self.fillCircleLayer { + self.fillCircleLayer = nil + fillCircleLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fillCircleLayer] _ in + fillCircleLayer?.removeFromSuperlayer() + }) + } + default: + break + } + } + + private let queue = Queue() + private func commit(interactive: Bool = false, synchronous: Bool = true, completion: @escaping () -> Void = {}) { + let currentImage = self.drawingImage + let uncommitedElement = self.uncommitedElement + let imageSize = self.imageSize + + let action = { + let updatedImage = self.renderer.image { context in + context.cgContext.setBlendMode(.copy) + context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) + if let image = currentImage { + image.draw(at: .zero) + } + if let uncommitedElement = uncommitedElement { + context.cgContext.setBlendMode(.normal) + uncommitedElement.draw(in: context.cgContext, size: imageSize) + } + } + Queue.mainQueue().async { + self.drawingImage = updatedImage + self.layer.contents = updatedImage.cgImage + + if let currentDrawingRenderView = self.currentDrawingRenderView { + if case .eraser = self.tool { + currentDrawingRenderView.removeFromSuperview() + self.mask = nil + self.insertSubview(self.currentDrawingViewContainer, at: 0) + self.currentDrawingViewContainer.backgroundColor = .clear + } else if case .blur = self.tool { + self.currentDrawingViewContainer.mask = nil + self.currentDrawingViewContainer.image = nil + } else { + currentDrawingRenderView.removeFromSuperview() + } + self.currentDrawingRenderView = nil + } + if let currentDrawingLayer = self.currentDrawingLayer { + if case .eraser = self.tool { + currentDrawingLayer.removeFromSuperlayer() + self.mask = nil + self.insertSubview(self.currentDrawingViewContainer, at: 0) + self.currentDrawingViewContainer.backgroundColor = .clear + } else if case .blur = self.tool { + self.currentDrawingViewContainer.layer.mask = nil + self.currentDrawingViewContainer.image = nil + } else { + currentDrawingLayer.removeFromSuperlayer() + } + self.currentDrawingLayer = nil + } + + if self.tool == .marker { + //self.metalView?.clear() + self.metalView?.isHidden = true + } + completion() + } + } + if synchronous { + action() + } else { + self.queue.async { + action() + } + } + } + + fileprivate func cancelDrawing() { + self.uncommitedElement = nil + + if let currentDrawingRenderView = self.currentDrawingRenderView { + if case .eraser = self.tool { + currentDrawingRenderView.removeFromSuperview() + self.mask = nil + self.insertSubview(self.currentDrawingViewContainer, at: 0) + self.currentDrawingViewContainer.backgroundColor = .clear + } else if case .blur = self.tool { + self.currentDrawingViewContainer.mask = nil + self.currentDrawingViewContainer.image = nil + } else { + currentDrawingRenderView.removeFromSuperview() + } + self.currentDrawingRenderView = nil + } + if let currentDrawingLayer = self.currentDrawingLayer { + if self.tool == .eraser { + currentDrawingLayer.removeFromSuperlayer() + self.mask = nil + self.insertSubview(self.currentDrawingViewContainer, at: 0) + self.currentDrawingViewContainer.backgroundColor = .clear + } else if self.tool == .blur { + self.currentDrawingViewContainer.mask = nil + self.currentDrawingViewContainer.image = nil + } else { + currentDrawingLayer.removeFromSuperlayer() + } + self.currentDrawingLayer = nil + } + if case .marker = self.tool { + self.metalView?.isHidden = true + } + } + + private func slice(for rect: CGRect) -> DrawingSlice? { + if let subImage = self.drawingImage?.cgImage?.cropping(to: rect) { + return DrawingSlice(image: subImage, rect: rect) + } + return nil + } + + fileprivate func finishDrawing(rect: CGRect, synchronous: Bool = false) { + let complete: (Bool) -> Void = { synchronous in + if let uncommitedElement = self.uncommitedElement, !uncommitedElement.isValid { + self.uncommitedElement = nil + } + if !self.undoStack.isEmpty || self.hasOpaqueData, let slice = self.slice(for: rect) { + self.undoStack.append(.slice(slice)) + } else { + self.undoStack.append(.clearAll(rect)) + } + + self.commit(interactive: true, synchronous: synchronous) + + self.redoStack.removeAll() + self.uncommitedElement = nil + + self.updateInternalState() + } + if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.hasArrow { + uncommitedElement.finishArrow({ + complete(true) + }) + } else { + complete(synchronous) + } + } + + weak var entitiesView: DrawingEntitiesView? + func clear() { + self.entitiesView?.removeAll() + + self.uncommitedElement = nil + self.undoStack.removeAll() + self.redoStack.removeAll() + self.hasOpaqueData = false + + let snapshotView = UIImageView(image: self.drawingImage) + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + + self.drawingImage = nil + self.layer.contents = nil + + Queue.mainQueue().justDispatch { + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.updateInternalState() + + self.updateBlurredImage() + } + + private func applySlice(_ slice: DrawingSlice) { + let updatedImage = self.renderer.image { context in + context.cgContext.clear(CGRect(origin: .zero, size: imageSize)) + context.cgContext.setBlendMode(.copy) + if let image = self.drawingImage { + image.draw(at: .zero) + } + if let image = slice.image { + context.cgContext.translateBy(x: imageSize.width / 2.0, y: imageSize.height / 2.0) + context.cgContext.scaleBy(x: 1.0, y: -1.0) + context.cgContext.translateBy(x: -imageSize.width / 2.0, y: -imageSize.height / 2.0) + context.cgContext.translateBy(x: slice.rect.minX, y: imageSize.height - slice.rect.maxY) + context.cgContext.draw(image, in: CGRect(origin: .zero, size: slice.rect.size)) + } + } + self.drawingImage = updatedImage + self.layer.contents = updatedImage.cgImage + } + + var canUndo: Bool { + return !self.undoStack.isEmpty + } + + private func undo() { + guard let lastOperation = self.undoStack.last else { + return + } + switch lastOperation { + case let .clearAll(rect): + if let slice = self.slice(for: rect) { + self.redoStack.append(.slice(slice)) + } + UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { + self.drawingImage = nil + self.layer.contents = nil + } + self.updateBlurredImage() + case let .slice(slice): + if let slice = self.slice(for: slice.rect) { + self.redoStack.append(.slice(slice)) + } + UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { + self.applySlice(slice) + } + self.updateBlurredImage() + case let .addEntity(uuid): + if let entityView = self.entitiesView?.getView(for: uuid) { + self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) + self.redoStack.append(.removeEntity(entityView.entity)) + } + case let .removeEntity(entity): + if let view = self.entitiesView?.add(entity, announce: false) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if !(entity is DrawingVectorEntity) { + view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + } + } + self.redoStack.append(.addEntity(entity.uuid)) + } + + self.undoStack.removeLast() + + self.updateInternalState() + } + + private func redo() { + guard let lastOperation = self.redoStack.last else { + return + } + + switch lastOperation { + case .clearAll: + break + case let .slice(slice): + if !self.undoStack.isEmpty || self.hasOpaqueData, let slice = self.slice(for: slice.rect) { + self.undoStack.append(.slice(slice)) + } else { + self.undoStack.append(.clearAll(slice.rect)) + } + UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { + self.applySlice(slice) + } + self.updateBlurredImage() + case let .addEntity(uuid): + if let entityView = self.entitiesView?.getView(for: uuid) { + self.entitiesView?.remove(uuid: uuid, animated: true, announce: false) + self.undoStack.append(.removeEntity(entityView.entity)) + } + case let .removeEntity(entity): + if let view = self.entitiesView?.add(entity, announce: false) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if !(entity is DrawingVectorEntity) { + view.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + } + } + self.undoStack.append(.addEntity(entity.uuid)) + } + + self.redoStack.removeLast() + + self.updateInternalState() + } + + func onEntityAdded(_ entity: DrawingEntity) { + self.redoStack.removeAll() + self.undoStack.append(.addEntity(entity.uuid)) + + self.updateInternalState() + } + + func onEntityRemoved(_ entity: DrawingEntity) { + self.redoStack.removeAll() + self.undoStack.append(.removeEntity(entity)) + + self.updateInternalState() + } + + private var preparedBlurredImage: UIImage? + + func updateToolState(_ state: DrawingToolState) { + let previousTool = self.tool + switch state { + case let .pen(brushState): + self.tool = .pen + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + case let .arrow(brushState): + self.tool = .arrow + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + case let .marker(brushState): + self.tool = .marker + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + + var size = self.imageSize + if Int(size.width) % 16 != 0 { + size.width = ceil(size.width / 16.0) * 16.0 + } + + if self.metalView == nil, let metalView = DrawingMetalView(size: size) { + metalView.transform = self.currentDrawingViewContainer.transform + if size.width != self.imageSize.width { + let scaledSize = size.preciseAspectFilled(self.currentDrawingViewContainer.frame.size) + metalView.frame = CGRect(origin: .zero, size: scaledSize) + } else { + metalView.frame = self.currentDrawingViewContainer.frame + } + self.insertSubview(metalView, aboveSubview: self.currentDrawingViewContainer) + self.metalView = metalView + } + case let .neon(brushState): + self.tool = .neon + self.toolColor = brushState.color + self.toolBrushSize = brushState.size + case let .blur(blurState): + self.tool = .blur + self.toolBrushSize = blurState.size + case let .eraser(eraserState): + self.tool = .eraser + self.toolBrushSize = eraserState.size + } + + if self.tool != previousTool { + self.updateBlurredImage() + } + } + + func updateBlurredImage() { + if case .blur = self.tool { + Queue.concurrentDefaultQueue().async { + if let image = self.getFullImage() { + Queue.mainQueue().async { + self.preparedBlurredImage = image + } + } + } + } else { + self.preparedBlurredImage = nil + } + } + + func performAction(_ action: Action) { + switch action { + case .undo: + self.undo() + case .redo: + self.redo() + case .clear: + self.clear() + case .zoomOut: + self.zoomOut() + } + } + + private func updateInternalState() { + self.stateUpdated(NavigationState( + canUndo: !self.undoStack.isEmpty, + canRedo: !self.redoStack.isEmpty, + canClear: !self.undoStack.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true), + canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, + isDrawing: self.isDrawing + )) + } + + public func updateZoomScale(_ scale: CGFloat) { + self.cancelDrawing() + self.zoomScale = scale + self.updateInternalState() + } + + private func setupNewElement() -> DrawingElement? { + let scale = 1.0 / self.zoomScale + let element: DrawingElement? + switch self.tool { + case .pen: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: false, + isEraser: false, + isBlur: false, + blurredImage: nil + ) + element = penTool + case .arrow: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: true, + isEraser: false, + isBlur: false, + blurredImage: nil + ) + element = penTool + case .marker: + let markerTool = MarkerTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale + ) + markerTool.metalView = self.metalView + element = markerTool + case .neon: + element = NeonTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale + ) + case .blur: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: false, + isEraser: false, + isBlur: true, + blurredImage: self.preparedBlurredImage + ) + element = penTool + case .eraser: + let penTool = PenTool( + drawingSize: self.imageSize, + color: self.toolColor, + lineWidth: self.toolBrushSize * scale, + hasArrow: false, + isEraser: true, + isBlur: false, + blurredImage: nil + ) + element = penTool + } + return element + } + + func setBrushSizePreview(_ size: CGFloat?) { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let size = size { + let minLineWidth = max(1.0, max(self.frame.width, self.frame.height) * 0.002) + let maxLineWidth = max(10.0, max(self.frame.width, self.frame.height) * 0.07) + + let minBrushSize = minLineWidth + let maxBrushSize = maxLineWidth + let brushSize = minBrushSize + (maxBrushSize - minBrushSize) * size + + self.brushSizePreviewLayer.transform = CATransform3DMakeScale(brushSize / 100.0, brushSize / 100.0, 1.0) + transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 1.0) + } else { + transition.setAlpha(layer: self.brushSizePreviewLayer, alpha: 0.0) + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let scale = self.scale + let transform = CGAffineTransformMakeScale(scale, scale) + self.currentDrawingViewContainer.transform = transform + self.currentDrawingViewContainer.frame = self.bounds + + self.drawingGesturePipeline?.transform = CGAffineTransformMakeScale(1.0 / scale, 1.0 / scale) + + if let metalView = self.metalView { + var size = self.imageSize + if Int(size.width) % 16 != 0 { + size.width = ceil(size.width / 16.0) * 16.0 + } + metalView.transform = transform + if size.width != self.imageSize.width { + let scaledSize = size.preciseAspectFilled(self.currentDrawingViewContainer.frame.size) + metalView.frame = CGRect(origin: .zero, size: scaledSize) + } else { + metalView.frame = self.currentDrawingViewContainer.frame + } + } + + self.brushSizePreviewLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + } + + public var isEmpty: Bool { + return self.undoStack.isEmpty && !self.hasOpaqueData + } + + public var scale: CGFloat { + return self.bounds.width / self.imageSize.width + } + + public var isTracking: Bool { + return self.uncommitedElement != nil + } +} + +private extension CGSize { + func preciseAspectFilled(_ size: CGSize) -> CGSize { + let scale = max(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) + return CGSize(width: self.width * scale, height: self.height * scale) + } +} + +private class DrawingSlice { + private static let queue = Queue() + + var _image: CGImage? + + let uuid: UUID + var image: CGImage? { + if let image = self._image { + return image + } else if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) { + return UIImage(data: data)?.cgImage + } else { + return nil + } + } + let rect: CGRect + let path: String + + init(image: CGImage, rect: CGRect) { + self.uuid = UUID() + + self._image = image + self.rect = rect + self.path = NSTemporaryDirectory() + "/drawing_\(uuid.hashValue).slice" + + DrawingSlice.queue.after(2.0) { + let image = UIImage(cgImage: image) + if let data = image.pngData() as? NSData { + try? data.write(toFile: self.path) + Queue.mainQueue().async { + self._image = nil + } + } + } + } + + deinit { + try? FileManager.default.removeItem(atPath: self.path) + } +} diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift new file mode 100644 index 00000000000..90181b3d32c --- /dev/null +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -0,0 +1,243 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit + +private let size = CGSize(width: 148.0, height: 148.0) +private let outerWidth: CGFloat = 12.0 +private let ringWidth: CGFloat = 5.0 +private let selectionWidth: CGFloat = 4.0 + +private func generateShadowImage(size: CGSize) -> UIImage? { + let inset: CGFloat = 60.0 + let imageSize = CGSize(width: size.width + inset * 2.0, height: size.height + inset * 2.0) + return generateImage(imageSize, rotatedContext: { imageSize, context in + context.clear(CGRect(origin: .zero, size: imageSize)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 40.0, color: UIColor(rgb: 0x000000, alpha: 0.9).cgColor) + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: inset, y: inset), size: size)) + }) +} + +private func generateGridImage(size: CGSize, light: Bool) -> UIImage? { + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(light ? UIColor.white.cgColor : UIColor(rgb: 0x505050).cgColor) + + let lineWidth: CGFloat = 1.0 + var offset: CGFloat = 7.0 + for _ in 0 ..< 8 { + context.fill(CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: size.width, height: lineWidth))) + context.fill(CGRect(origin: CGPoint(x: offset, y: 0.0), size: CGSize(width: lineWidth, height: size.height))) + + offset += 14.0 + } + }) +} + +final class EyedropperView: UIView { + private weak var drawingView: DrawingView? + + private let containerView: UIView + private let shadowLayer: SimpleLayer + private let clipView: UIView + private let zoomedView: UIImageView + + private let gridLayer: SimpleLayer + + private let outerColorLayer: SimpleLayer + private let ringLayer: SimpleLayer + private let selectionLayer: SimpleLayer + + private let sourceImage: (data: Data, size: CGSize, bytesPerRow: Int, info: CGBitmapInfo)? + + var completed: (DrawingColor) -> Void = { _ in } + var dismissed: () -> Void = { } + + init(containerSize: CGSize, drawingView: DrawingView, sourceImage: UIImage) { + self.drawingView = drawingView + + self.zoomedView = UIImageView(image: sourceImage) + self.zoomedView.isOpaque = true + self.zoomedView.layer.magnificationFilter = .nearest + + if let cgImage = sourceImage.cgImage, let pixelData = cgImage.dataProvider?.data as? Data { + self.sourceImage = (pixelData, sourceImage.size, cgImage.bytesPerRow, cgImage.bitmapInfo) + } else { + self.sourceImage = nil + } + + let bounds = CGRect(origin: .zero, size: size) + + self.containerView = UIView() + self.containerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - size.width) / 2.0), y: floorToScreenPixels((containerSize.height - size.height) / 2.0)), size: size) + + self.shadowLayer = SimpleLayer() + self.shadowLayer.contents = generateShadowImage(size: size)?.cgImage + self.shadowLayer.frame = bounds.insetBy(dx: -60.0, dy: -60.0) + + let clipFrame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth) + self.clipView = UIView() + self.clipView.clipsToBounds = true + self.clipView.frame = bounds.insetBy(dx: outerWidth + ringWidth, dy: outerWidth + ringWidth) + self.clipView.layer.cornerRadius = size.width / 2.0 - outerWidth - ringWidth + if #available(iOS 13.0, *) { + self.clipView.layer.cornerCurve = .circular + } + self.clipView.addSubview(self.zoomedView) + + self.gridLayer = SimpleLayer() + self.gridLayer.opacity = 0.6 + + self.gridLayer.frame = self.clipView.bounds + self.gridLayer.contents = generateGridImage(size: clipFrame.size, light: true)?.cgImage + + self.outerColorLayer = SimpleLayer() + self.outerColorLayer.rasterizationScale = UIScreen.main.scale + self.outerColorLayer.shouldRasterize = true + self.outerColorLayer.frame = bounds + self.outerColorLayer.cornerRadius = self.outerColorLayer.frame.width / 2.0 + self.outerColorLayer.borderWidth = outerWidth + + self.ringLayer = SimpleLayer() + self.ringLayer.rasterizationScale = UIScreen.main.scale + self.ringLayer.shouldRasterize = true + self.ringLayer.borderColor = UIColor.white.cgColor + self.ringLayer.frame = bounds.insetBy(dx: outerWidth, dy: outerWidth) + self.ringLayer.cornerRadius = self.ringLayer.frame.width / 2.0 + self.ringLayer.borderWidth = ringWidth + + self.selectionLayer = SimpleLayer() + self.selectionLayer.borderColor = UIColor.white.cgColor + self.selectionLayer.borderWidth = selectionWidth + self.selectionLayer.cornerRadius = 2.0 + self.selectionLayer.frame = CGRect(origin: CGPoint(x: clipFrame.minX + 48.0, y: clipFrame.minY + 48.0), size: CGSize(width: 17.0, height: 17.0)).insetBy(dx: -UIScreenPixel, dy: -UIScreenPixel) + + super.init(frame: .zero) + + self.addSubview(self.containerView) + self.clipView.layer.addSublayer(self.gridLayer) + + self.containerView.layer.addSublayer(self.shadowLayer) + self.containerView.addSubview(self.clipView) + self.containerView.layer.addSublayer(self.ringLayer) + self.containerView.layer.addSublayer(self.outerColorLayer) + self.containerView.layer.addSublayer(self.selectionLayer) + + self.containerView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecognizer) + + Queue.mainQueue().justDispatch { + self.updateColor() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var gridIsLight = true + private var currentColor: DrawingColor? + func setColor(_ color: UIColor) { + self.currentColor = DrawingColor(color: color) + self.outerColorLayer.borderColor = color.cgColor + self.selectionLayer.backgroundColor = color.cgColor + + if color.lightness > 0.9 { + self.ringLayer.borderColor = UIColor(rgb: 0x999999).cgColor + if self.gridIsLight { + self.gridIsLight = false + self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: false)?.cgImage + } + } else { + self.ringLayer.borderColor = UIColor.white.cgColor + if !self.gridIsLight { + self.gridIsLight = true + self.gridLayer.contents = generateGridImage(size: self.clipView.frame.size, light: true)?.cgImage + } + } + } + + func dismiss() { + self.containerView.alpha = 0.0 + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + self.dismissed() + } + + private func getColorAt(_ point: CGPoint) -> UIColor? { + guard var sourceImage = self.sourceImage, point.x >= 0 && point.x < sourceImage.size.width && point.y >= 0 && point.y < sourceImage.size.height else { + return UIColor.black + } + + let x = Int(point.x) + let y = Int(point.y) + + var color: UIColor? + sourceImage.data.withUnsafeMutableBytes { buffer in + guard let bytes = buffer.assumingMemoryBound(to: UInt8.self).baseAddress else { + return + } + + let srcLine = bytes.advanced(by: y * sourceImage.bytesPerRow) + let srcPixel = srcLine + x * 4 + let r = srcPixel.pointee + let g = srcPixel.advanced(by: 1).pointee + let b = srcPixel.advanced(by: 2).pointee + + if sourceImage.info.contains(.byteOrder32Little) { + color = UIColor(red: CGFloat(b) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(r) / 255.0, alpha: 1.0) + } else { + color = UIColor(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0) + } + } + return color + } + + private func updateColor() { + guard let drawingView = self.drawingView else { + return + } + var point = self.convert(self.containerView.center, to: drawingView) + point.x /= drawingView.scale + point.y /= drawingView.scale + + let scale: CGFloat = 15.0 + self.zoomedView.transform = CGAffineTransformMakeScale(scale, scale) + self.zoomedView.center = CGPoint(x: self.clipView.frame.width / 2.0 + (self.zoomedView.bounds.width / 2.0 - point.x) * scale, y: self.clipView.frame.height / 2.0 + (self.zoomedView.bounds.height / 2.0 - point.y) * scale) + + if let color = self.getColorAt(point) { + self.setColor(color) + } + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + switch gestureRecognizer.state { + case .changed: + let translation = gestureRecognizer.translation(in: self) + self.containerView.center = self.containerView.center.offsetBy(dx: translation.x, dy: translation.y) + gestureRecognizer.setTranslation(.zero, in: self) + + self.updateColor() + case .ended, .cancelled: + if let color = currentColor { + self.containerView.alpha = 0.0 + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + self.containerView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + + self.completed(color) + } + default: + break + } + } +} diff --git a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift new file mode 100644 index 00000000000..b4c037ec8ff --- /dev/null +++ b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift @@ -0,0 +1,266 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import SegmentedControlNode + +private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath { + let path = UIBezierPath() + path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true) + path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true) + path.close() + return path +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 28.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) +} + +class ModeAndSizeComponent: Component { + let values: [String] + let sizeValue: CGFloat + let isEditing: Bool + let isEnabled: Bool + let rightInset: CGFloat + let tag: AnyObject? + let selectedIndex: Int + let selectionChanged: (Int) -> Void + let sizeUpdated: (CGFloat) -> Void + let sizeReleased: () -> Void + + init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) { + self.values = values + self.sizeValue = sizeValue + self.isEditing = isEditing + self.isEnabled = isEnabled + self.rightInset = rightInset + self.tag = tag + self.selectedIndex = selectedIndex + self.selectionChanged = selectionChanged + self.sizeUpdated = sizeUpdated + self.sizeReleased = sizeReleased + } + + static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool { + if lhs.values != rhs.values { + return false + } + if lhs.sizeValue != rhs.sizeValue { + return false + } + if lhs.isEditing != rhs.isEditing { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.rightInset != rhs.rightInset { + return false + } + if lhs.selectedIndex != rhs.selectedIndex { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { + private let backgroundNode: NavigationBackgroundNode + private let node: SegmentedControlNode + + private var knob: UIImageView + + private let maskLayer = SimpleShapeLayer() + + private var isEditing: Bool? + private var isControlEnabled: Bool? + private var sliderWidth: CGFloat = 0.0 + + fileprivate var updated: (CGFloat) -> Void = { _ in } + fileprivate var released: () -> Void = { } + + private var component: ModeAndSizeComponent? + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + init() { + self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) + self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0) + + self.knob = UIImageView(image: generateKnobImage()) + + super.init(frame: CGRect()) + + self.layer.allowsGroupOpacity = true + + self.addSubview(self.backgroundNode.view) + self.addSubview(self.node.view) + self.addSubview(self.knob) + + self.backgroundNode.layer.mask = self.maskLayer + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + panGestureRecognizer.delegate = self + self.addGestureRecognizer(panGestureRecognizer) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) + guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) + self.updated(value) + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + switch gestureRecognizer.state { + case .changed: + let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) + guard self.frame.width > 0.0 else { + return + } + let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.released() + default: + break + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled { + return isEditing && isControlEnabled + } else { + return false + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateIn() { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + func animateOut() { + self.node.alpha = 0.0 + self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + self.backgroundNode.alpha = 0.0 + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + + func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + self.updated = component.sizeUpdated + self.released = component.sizeReleased + + let previousIsEditing = self.isEditing + self.isEditing = component.isEditing + self.isControlEnabled = component.isEnabled + + if component.isEditing { + self.sliderWidth = availableSize.width + } + + self.node.items = component.values.map { SegmentedControlItem(title: $0) } + self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate) + let selectionChanged = component.selectionChanged + self.node.selectedIndexChanged = { [weak self] index in + self?.window?.endEditing(true) + selectionChanged(index) + } + + let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition) + let size = CGSize(width: availableSize.width, height: nodeSize.height) + transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize)) + + var isDismissingEditing = false + if component.isEditing != previousIsEditing && !component.isEditing { + isDismissingEditing = true + } + + self.knob.alpha = component.isEditing ? 1.0 : 0.0 + if !isDismissingEditing { + self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size) + } + + if component.isEditing != previousIsEditing { + let containedTransition = transition.containedViewLayoutTransition + let maskPath: UIBezierPath + if component.isEditing { + maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5) + let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition) + containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) + + self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0) + if previousIsEditing != nil { + let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition) + containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) + self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath) + } + + transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize)) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn() + case .animateOut: + self.animateOut() + } + } + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift new file mode 100644 index 00000000000..74cee9670ca --- /dev/null +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -0,0 +1,1038 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import ViewControllerComponent +import EntityKeyboard +import PagerComponent +import FeaturedStickersScreen +import TelegramNotices +import ChatEntityKeyboardInputNode +import ContextUI + +struct StickerPickerInputData: Equatable { + var emoji: EmojiPagerContentComponent + var stickers: EmojiPagerContentComponent? + var masks: EmojiPagerContentComponent? + + init( + emoji: EmojiPagerContentComponent, + stickers: EmojiPagerContentComponent?, + masks: EmojiPagerContentComponent? + ) { + self.emoji = emoji + self.stickers = stickers + self.masks = masks + } +} + +private final class StickerSelectionComponent: Component { + typealias EnvironmentType = Empty + + let theme: PresentationTheme + let strings: PresentationStrings + let deviceMetrics: DeviceMetrics + let bottomInset: CGFloat + let content: StickerPickerInputData + let backgroundColor: UIColor + let separatorColor: UIColor + + init( + theme: PresentationTheme, + strings: PresentationStrings, + deviceMetrics: DeviceMetrics, + bottomInset: CGFloat, + content: StickerPickerInputData, + backgroundColor: UIColor, + separatorColor: UIColor + ) { + self.theme = theme + self.strings = strings + self.deviceMetrics = deviceMetrics + self.bottomInset = bottomInset + self.content = content + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + } + + public static func ==(lhs: StickerSelectionComponent, rhs: StickerSelectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings != rhs.strings { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.separatorColor != rhs.separatorColor { + return false + } + return true + } + + public final class View: UIView { + private let keyboardView: ComponentView + private let keyboardClippingView: UIView + private let panelHostView: PagerExternalTopPanelContainer + private let panelBackgroundView: BlurredBackgroundView + private let panelSeparatorView: UIView + + private var component: StickerSelectionComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.keyboardView = ComponentView() + self.keyboardClippingView = UIView() + self.panelHostView = PagerExternalTopPanelContainer() + self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.panelSeparatorView = UIView() + + super.init(frame: frame) + + self.addSubview(self.keyboardClippingView) + self.addSubview(self.panelBackgroundView) + self.addSubview(self.panelSeparatorView) + self.addSubview(self.panelHostView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.backgroundColor = component.backgroundColor + let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) + self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) + self.panelSeparatorView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1) + + self.component = component + self.state = state + + let topPanelHeight: CGFloat = 42.0 + + let keyboardSize = self.keyboardView.update( + transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), + component: AnyComponent(EntityKeyboardComponent( + theme: component.theme, + strings: component.strings, + isContentInFocus: true, + containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: component.bottomInset, right: 0.0), + topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), + emojiContent: component.content.emoji, + stickerContent: component.content.stickers, + maskContent: component.content.masks, + gifContent: nil, + hasRecentGifs: false, + availableGifSearchEmojies: [], + defaultToEmojiTab: false, + externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: true, + topPanelExtensionUpdated: { _, _ in }, + hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { _, _ in }, + switchToTextInput: {}, + switchToGifSubject: { _ in }, + reorderItems: { _, _ in }, + makeSearchContainerNode: { _ in return nil }, + deviceMetrics: component.deviceMetrics, + hiddenInputHeight: 0.0, + inputHeight: 0.0, + displayBottomPanel: true, + isExpanded: true, + clipContentToTopPanel: false + )), + environment: {}, + containerSize: availableSize + ) + if let keyboardComponentView = self.keyboardView.view { + if keyboardComponentView.superview == nil { + self.keyboardClippingView.addSubview(keyboardComponentView) + } + + if panelBackgroundColor.alpha < 0.01 { + self.keyboardClippingView.clipsToBounds = true + } else { + self.keyboardClippingView.clipsToBounds = false + } + + transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight))) + + transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize)) + transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) + + transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) + self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) + transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +class StickerPickerScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: StickerPickerScreen? + private let theme: PresentationTheme + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let hostView: ComponentHostView + + private var content: StickerPickerInputData? + private let contentDisposable = MetaDisposable() + private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + fileprivate var temporaryDismiss = false + + init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.controller = controller + self.theme = theme + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = SparseContainerView() + self.containerView = SparseContainerView() + self.hostView = ComponentHostView() + + super.init() + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = .clear + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.hostView) + + self.contentDisposable.set(controller.inputData.start(next: { [weak self] inputData in + if let strongSelf = self { + strongSelf.updateContent(inputData) + } + })) + } + + deinit { + self.contentDisposable.dispose() + } + + func updateContent(_ content: StickerPickerInputData) { + self.content = content + + content.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let strongSelf = self, let file = item.itemFile else { + return + } + strongSelf.controller?.completion(file) + strongSelf.controller?.dismiss(animated: true) + }, + deleteBackwards: nil, + openStickerSettings: nil, + openFeatured: nil, + openSearch: { + }, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { [weak self] groupId in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + if groupId == AnyHashable("popular") { + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + let context = controller.context + items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true)) + items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + guard let strongSelf = self else { + return + } + + strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular")) + let _ = context.engine.stickers.clearRecentlyUsedReactions().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) + } + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }, + requestUpdate: { _ in + }, + updateSearchQuery: { _, _ in + }, + updateScrollingToItemGroup: { [weak self] in + self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false, + hideBackground: true + ) + + content.masks?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let strongSelf = self, let file = item.itemFile else { + return + } + strongSelf.controller?.completion(file) + strongSelf.controller?.dismiss(animated: true) + }, + deleteBackwards: nil, + openStickerSettings: nil, + openFeatured: nil, + openSearch: {}, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { _ in + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }, + requestUpdate: { _ in + }, + updateSearchQuery: { _, _ in + }, + updateScrollingToItemGroup: { [weak self] in + self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + }, + chatPeerId: nil, + peekBehavior: nil, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false, + hideBackground: true + ) + + var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? + if let controller = self.controller { + stickerPeekBehavior = EmojiContentPeekBehaviorImpl( + context: controller.context, + interaction: nil, + chatPeerId: nil, + present: { [weak controller] c, a in + controller?.presentInGlobalOverlay(c, with: a) + } + ) + } + + content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak self] _, item, _, _, _, _ in + guard let strongSelf = self, let file = item.itemFile else { + return + } + strongSelf.controller?.completion(file) + strongSelf.controller?.dismiss(animated: true) + }, + deleteBackwards: nil, + openStickerSettings: nil, + openFeatured: nil, + openSearch: { + }, + addGroupAction: { [weak self] groupId, isPremiumLocked in + guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { + return + } + let context = controller.context + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredStickerPack.info.id == collectionId { + let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue).start(completed: { + }) + + break + } + } + }) + }, + clearGroup: { [weak self] groupId in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let context = controller.context + if groupId == AnyHashable("recent") { + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = context.engine.stickers.clearRecentlyUsedStickers().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) + } else if groupId == AnyHashable("featuredTop") { + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + var stickerPackIds: [Int64] = [] + for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + stickerPackIds.append(featuredStickerPack.info.id.id) + } + let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start() + }) + } else if groupId == AnyHashable("peerSpecific") { + } + }, + pushController: { c in + }, + presentController: { c in + }, + presentGlobalOverlayController: { c in + }, + navigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }, + requestUpdate: { _ in + }, + updateSearchQuery: { _, _ in + }, + updateScrollingToItemGroup: { [weak self] in + self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + }, + chatPeerId: nil, + peekBehavior: stickerPeekBehavior, + customLayout: nil, + externalBackground: nil, + externalExpansionView: nil, + useOpaqueTheme: false, + hideBackground: true + ) + + if let (layout, navigationHeight) = self.currentLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.completion(nil) + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let (layout, _) = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + if otherGestureRecognizer is PagerPanGestureRecognizer { + return false + } else if otherGestureRecognizer is UIPanGestureRecognizer, let scrollView = otherGestureRecognizer.view, scrollView.frame.width > scrollView.frame.height { + return false + } else if otherGestureRecognizer is PeekControllerGestureRecognizer { + return false + } + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + if !self.temporaryDismiss { + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + self.currentLayout = (layout, navigationHeight) + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + var bottomInset = layout.intrinsicInsets.bottom + if let (panInitialTopInset, panOffset, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + let contentFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + contentFrame = clipFrame + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + contentFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height - topInset) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: floorToScreenPixels(min(layout.size.width - 20.0, floor(maxSide / 2.0)) * 0.66), height: floorToScreenPixels((min(layout.size.height, minSide) - verticalInset * 2.0) * 0.66)) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + contentFrame = clipFrame + + bottomInset = 0.0 + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + + if let content = self.content { + var stickersTransition: Transition = transition + if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { + self.scheduledEmojiContentAnimationHint = nil + let contentAnimation = scheduledEmojiContentAnimationHint + stickersTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) + } + + var contentSize = self.hostView.update( + transition: stickersTransition, + component: AnyComponent( + StickerSelectionComponent( + theme: self.theme, + strings: self.presentationData.strings, + deviceMetrics: layout.deviceMetrics, + bottomInset: bottomInset, + content: content, + backgroundColor: self.theme.list.itemBlocksBackgroundColor, + separatorColor: self.theme.list.blocksBackgroundColor + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: contentFrame.size.width, height: contentFrame.height) + ) + contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + } + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let (layout, _) = self.currentLayout else{ + return 210.0 + } + if case .compact = layout.metrics.widthClass { + var factor: CGFloat = 0.2488 + if layout.size.width <= 320.0 { + factor = 0.15 + } + return floor(max(layout.size.width, layout.size.height) * factor) + } else { + return 210.0 + } + } + + private func findScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? PagerExpandableScrollView { + return view + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollView = self.findScrollView(view: currentHitView) + if scrollView?.frame.height == self.frame.width { + scrollView = nil + } + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView) + case .changed: + guard let (topInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + self.panGestureArguments = (topInset, translation, scrollView) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.completion(nil) + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let theme: PresentationTheme + private let inputData: Signal + + private var currentLayout: ContainerViewLayout? + + public var pushController: (ViewController) -> Void = { _ in } + public var presentController: (ViewController) -> Void = { _ in } + + var completion: (TelegramMediaFile?) -> Void = { _ in } + + init(context: AccountContext, inputData: Signal) { + self.context = context + self.theme = defaultDarkColorPresentationTheme + self.inputData = inputData + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, theme: self.theme) + self.displayNodeDidLoad() + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift new file mode 100644 index 00000000000..59ffd9bf2e7 --- /dev/null +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -0,0 +1,770 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import LegacyComponents +import TelegramCore +import Postbox +import LottieAnimationComponent + +enum DrawingTextStyle: Equatable { + case regular + case filled + case semi + case stroke + + init(style: DrawingTextEntity.Style) { + switch style { + case .regular: + self = .regular + case .filled: + self = .filled + case .semi: + self = .semi + case .stroke: + self = .stroke + } + } +} + +enum DrawingTextAlignment: Equatable { + case left + case center + case right + + init(alignment: DrawingTextEntity.Alignment) { + switch alignment { + case .left: + self = .left + case .center: + self = .center + case .right: + self = .right + } + } +} + +enum DrawingTextFont: Equatable, Hashable { + case sanFrancisco + case other(String, String) + + init(font: DrawingTextEntity.Font) { + switch font { + case .sanFrancisco: + self = .sanFrancisco + case let .other(font, name): + self = .other(font, name) + } + } + + var font: DrawingTextEntity.Font { + switch self { + case .sanFrancisco: + return .sanFrancisco + case let .other(font, name): + return .other(font, name) + } + } + + var title: String { + switch self { + case .sanFrancisco: + return "San Francisco" + case let .other(_, name): + return name + } + } + + func uiFont(size: CGFloat) -> UIFont { + switch self { + case .sanFrancisco: + return Font.with(size: size, design: .round, weight: .semibold) + case let .other(font, _): + return UIFont(name: font, size: size) ?? Font.semibold(size) + } + } +} + +final class TextAlignmentComponent: Component { + let alignment: DrawingTextAlignment + + init(alignment: DrawingTextAlignment) { + self.alignment = alignment + } + + static func == (lhs: TextAlignmentComponent, rhs: TextAlignmentComponent) -> Bool { + return lhs.alignment == rhs.alignment + } + + public final class View: UIView { + private let line1 = SimpleLayer() + private let line2 = SimpleLayer() + private let line3 = SimpleLayer() + private let line4 = SimpleLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + let lines = [self.line1, self.line2, self.line3, self.line4] + lines.forEach { line in + line.backgroundColor = UIColor.white.cgColor + line.cornerRadius = 1.0 + line.masksToBounds = true + self.layer.addSublayer(line) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TextAlignmentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height = 2.0 - UIScreenPixel + let spacing: CGFloat = 3.0 + UIScreenPixel + let long = 21.0 + let short = 13.0 + + let size = CGSize(width: long, height: 18.0) + + switch component.alignment { + case .left: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: 0.0, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + case .center: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - long) / 2.0), y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - short) / 2.0), y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + case .right: + transition.setFrame(layer: self.line1, frame: CGRect(origin: CGPoint(x: size.width - long, y: 0.0), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line2, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing), size: CGSize(width: short, height: height))) + transition.setFrame(layer: self.line3, frame: CGRect(origin: CGPoint(x: size.width - long, y: height + spacing + height + spacing), size: CGSize(width: long, height: height))) + transition.setFrame(layer: self.line4, frame: CGRect(origin: CGPoint(x: size.width - short, y: height + spacing + height + spacing + height + spacing), size: CGSize(width: short, height: height))) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TextFontComponent: Component { + let selectedValue: DrawingTextFont + let tag: AnyObject? + let tapped: () -> Void + + init(selectedValue: DrawingTextFont, tag: AnyObject?, tapped: @escaping () -> Void) { + self.selectedValue = selectedValue + self.tag = tag + self.tapped = tapped + } + + static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool { + return lhs.selectedValue == rhs.selectedValue + } + + final class View: UIView, ComponentTaggedView { + private var button = HighlightableButton() + private let icon = SimpleLayer() + + private var component: TextFontComponent? + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.button) + self.button.layer.addSublayer(self.icon) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed(_ sender: HighlightableButton) { + if let component = self.component { + component.tapped() + } + } + + func update(component: TextFontComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + if self.icon.contents == nil { + self.icon.contents = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/FontArrow"), color: UIColor(rgb: 0xffffff, alpha: 0.5))?.cgImage + } + + let value = component.selectedValue + + var disappearingSnapshotView: UIView? + let previousTitle = self.button.title(for: .normal) + if previousTitle != value.title { + if let snapshotView = self.button.titleLabel?.snapshotView(afterScreenUpdates: false) { + snapshotView.center = self.button.titleLabel?.center ?? snapshotView.center + self.button.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.button.titleLabel?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + disappearingSnapshotView = snapshotView + } + } + + self.button.clipsToBounds = true + self.button.setTitle(value.title, for: .normal) + self.button.titleLabel?.font = value.uiFont(size: 13.0) + self.button.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 26.0) + var buttonSize = self.button.sizeThatFits(availableSize) + buttonSize.width += 39.0 - 13.0 + buttonSize.height = 30.0 + transition.setFrame(view: self.button, frame: CGRect(origin: .zero, size: buttonSize)) + self.button.layer.cornerRadius = 11.0 + self.button.layer.borderWidth = 1.0 - UIScreenPixel + self.button.layer.borderColor = UIColor.white.cgColor + self.button.addTarget(self, action: #selector(self.pressed(_:)), for: .touchUpInside) + + let iconSize = CGSize(width: 16.0, height: 16.0) + let iconFrame = CGRect(origin: CGPoint(x: buttonSize.width - iconSize.width - 8.0, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize) + transition.setFrame(layer: self.icon, frame: iconFrame) + + if let disappearingSnapshotView, let titleLabel = self.button.titleLabel { + disappearingSnapshotView.layer.animatePosition(from: disappearingSnapshotView.center, to: titleLabel.center, duration: 0.2, removeOnCompletion: false) + self.button.titleLabel?.layer.animatePosition(from: disappearingSnapshotView.center, to: titleLabel.center, duration: 0.2) + } + + return CGSize(width: self.button.frame.width, height: availableSize.height) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class TextSettingsComponent: CombinedComponent { + let color: DrawingColor? + let style: DrawingTextStyle + let alignment: DrawingTextAlignment + let font: DrawingTextFont + let isEmojiKeyboard: Bool + let tag: AnyObject? + let fontTag: AnyObject? + + let presentColorPicker: () -> Void + let presentFastColorPicker: (GenericComponentViewTag) -> Void + let updateFastColorPickerPan: (CGPoint) -> Void + let dismissFastColorPicker: () -> Void + let toggleStyle: () -> Void + let toggleAlignment: () -> Void + let presentFontPicker: () -> Void + let toggleKeyboard: (() -> Void)? + + init( + color: DrawingColor?, + style: DrawingTextStyle, + alignment: DrawingTextAlignment, + font: DrawingTextFont, + isEmojiKeyboard: Bool, + tag: AnyObject?, + fontTag: AnyObject?, + presentColorPicker: @escaping () -> Void = {}, + presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in }, + updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in }, + dismissFastColorPicker: @escaping () -> Void = {}, + toggleStyle: @escaping () -> Void, + toggleAlignment: @escaping () -> Void, + presentFontPicker: @escaping () -> Void, + toggleKeyboard: (() -> Void)? + ) { + self.color = color + self.style = style + self.alignment = alignment + self.font = font + self.isEmojiKeyboard = isEmojiKeyboard + self.tag = tag + self.fontTag = fontTag + self.presentColorPicker = presentColorPicker + self.presentFastColorPicker = presentFastColorPicker + self.updateFastColorPickerPan = updateFastColorPickerPan + self.dismissFastColorPicker = dismissFastColorPicker + self.toggleStyle = toggleStyle + self.toggleAlignment = toggleAlignment + self.presentFontPicker = presentFontPicker + self.toggleKeyboard = toggleKeyboard + } + + static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.style != rhs.style { + return false + } + if lhs.alignment != rhs.alignment { + return false + } + if lhs.font != rhs.font { + return false + } + if lhs.isEmojiKeyboard != rhs.isEmojiKeyboard { + return false + } + return true + } + + final class State: ComponentState { + enum ImageKey: Hashable { + case regular + case filled + case semi + case stroke + case keyboard + case emoji + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .regular: + image = UIImage(bundleImageName: "Media Editor/TextDefault")! + case .filled: + image = UIImage(bundleImageName: "Media Editor/TextFilled")! + case .semi: + image = UIImage(bundleImageName: "Media Editor/TextSemi")! + case .stroke: + image = UIImage(bundleImageName: "Media Editor/TextStroke")! + case .keyboard: + image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: .white)! + case .emoji: + image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputEmojiIcon"), color: .white)! + } + cachedImages[key] = image + return image + } + } + } + + class View: UIView, ComponentTaggedView { + var componentTag: AnyObject? + + public func matches(tag: Any) -> Bool { + if let componentTag = self.componentTag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + func animateIn() { + var delay: Double = 0.0 + for view in self.subviews { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, delay: delay) + delay += 0.02 + } + } + + func animateOut(completion: @escaping () -> Void) { + var isFirst = true + for view in self.subviews { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: isFirst ? { _ in + completion() + } : nil) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + isFirst = false + } + } + } + + func makeView() -> View { + let view = View() + view.componentTag = self.tag + return view + } + + func makeState() -> State { + State() + } + + static var body: Body { + let colorButton = Child(ColorSwatchComponent.self) + let colorButtonTag = GenericComponentViewTag() + + let alignmentButton = Child(Button.self) + let styleButton = Child(Button.self) + let keyboardButton = Child(Button.self) + let font = Child(TextFontComponent.self) + + return { context in + let component = context.component + let state = context.state + + let toggleStyle = component.toggleStyle + let toggleAlignment = component.toggleAlignment + + var offset: CGFloat = 6.0 + if let color = component.color { + let presentColorPicker = component.presentColorPicker + let presentFastColorPicker = component.presentFastColorPicker + let updateFastColorPickerPan = component.updateFastColorPickerPan + let dismissFastColorPicker = component.dismissFastColorPicker + + let colorButton = colorButton.update( + component: ColorSwatchComponent( + type: .main, + color: color, + tag: colorButtonTag, + action: { + presentColorPicker() + }, + holdAction: { + presentFastColorPicker(colorButtonTag) + }, + pan: { point in + updateFastColorPickerPan(point) + }, + release: { + dismissFastColorPicker() + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: context.transition + ) + context.add(colorButton + .position(CGPoint(x: colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height / 2.0)) + ) + offset += 42.0 + } + + let styleImage: UIImage + switch component.style { + case .regular: + styleImage = state.image(.regular) + case .filled: + styleImage = state.image(.filled) + case .semi: + styleImage = state.image(.semi) + case .stroke: + styleImage = state.image(.stroke) + } + + var fontAvailableWidth: CGFloat = context.availableSize.width + if component.color != nil { + fontAvailableWidth -= 72.0 + } + + let styleButton = styleButton.update( + component: Button( + content: AnyComponent( + Image( + image: styleImage + ) + ), + action: { + toggleStyle() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .easeInOut(duration: 0.2) + ) + context.add(styleButton + .position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0)) + .update(Transition.Update { _, view, transition in + if let snapshot = view.snapshotView(afterScreenUpdates: false) { + transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperview() + }) + snapshot.frame = view.frame + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + view.superview?.addSubview(snapshot) + } + }) + ) + offset += 44.0 + + let alignmentButton = alignmentButton.update( + component: Button( + content: AnyComponent( + TextAlignmentComponent( + alignment: component.alignment + ) + ), + action: { + toggleAlignment() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: context.availableSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(alignmentButton + .position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0 + 1.0 - UIScreenPixel)) + ) + offset += 45.0 + + if let toggleKeyboard = component.toggleKeyboard { + let keyboardButton = keyboardButton.update( + component: Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem(name: !component.isEmojiKeyboard ? "input_anim_smileToKey" : "input_anim_keyToSmile" , mode: .animateTransitionFromPrevious), + colors: ["__allcolors__": UIColor.white], + size: CGSize(width: 32.0, height: 32.0) + ) + ), + action: { + toggleKeyboard() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 32.0, height: 32.0), + transition: .easeInOut(duration: 0.15) + ) + context.add(keyboardButton + .position(CGPoint(x: offset + keyboardButton.size.width / 2.0 + (component.isEmojiKeyboard ? 3.0 : 0.0), y: context.availableSize.height / 2.0)) + ) + } + + let font = font.update( + component: TextFontComponent( + selectedValue: component.font, + tag: component.fontTag, + tapped: { + component.presentFontPicker() + } + ), + availableSize: CGSize(width: fontAvailableWidth, height: 30.0), + transition: .easeInOut(duration: 0.2) + ) + context.add(font + .position(CGPoint(x: context.availableSize.width - font.size.width / 2.0 - 16.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +private func generateMaskPath(size: CGSize, topRadius: CGFloat, bottomRadius: CGFloat) -> UIBezierPath { + let path = UIBezierPath() + path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: topRadius), radius: topRadius, startAngle: .pi, endAngle: 0, clockwise: true) + path.addArc(withCenter: CGPoint(x: size.width / 2.0, y: size.height - bottomRadius), radius: bottomRadius, startAngle: 0, endAngle: .pi, clockwise: true) + path.close() + return path +} + +private func generateKnobImage() -> UIImage? { + let side: CGFloat = 32.0 + let margin: CGFloat = 10.0 + + let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) + }) + return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) +} + +final class TextSizeSliderComponent: Component { + let value: CGFloat + let tag: AnyObject? + let updated: (CGFloat) -> Void + let released: () -> Void + + public init( + value: CGFloat, + tag: AnyObject?, + updated: @escaping (CGFloat) -> Void, + released: @escaping () -> Void + ) { + self.value = value + self.tag = tag + self.updated = updated + self.released = released + } + + public static func ==(lhs: TextSizeSliderComponent, rhs: TextSizeSliderComponent) -> Bool { + if lhs.value != rhs.value { + return false + } + return true + } + + final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { + private var validSize: CGSize? + + private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) + private let maskLayer = SimpleShapeLayer() + + private let knobContainer = SimpleLayer() + private let knob = SimpleLayer() + + fileprivate var updated: (CGFloat) -> Void = { _ in } + fileprivate var released: () -> Void = { } + + private var component: TextSizeSliderComponent? + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + init() { + super.init(frame: CGRect()) + + self.layer.allowsGroupOpacity = true + self.isExclusiveTouch = true + + let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) + pressGestureRecognizer.minimumPressDuration = 0.01 + pressGestureRecognizer.delegate = self + self.addGestureRecognizer(pressGestureRecognizer) + self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + private var isTracking: Bool? + private var isPanning = false + private var isPressing = false + + @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { + guard self.frame.height > 0.0 else { + return + } + switch gestureRecognizer.state { + case .began: + self.isPressing = true + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + + let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0) + let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.isPressing = false + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + self.released() + default: + break + } + } + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard self.frame.height > 0.0 else { + return + } + switch gestureRecognizer.state { + case .began, .changed: + self.isPanning = true + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + let location = gestureRecognizer.location(in: self).offsetBy(dx: 0.0, dy: -12.0) + let value = 1.0 - max(0.0, min(1.0, location.y / (self.frame.height - 24.0))) + self.updated(value) + case .ended, .cancelled: + self.isPanning = false + if let size = self.validSize, let component = self.component { + let _ = self.updateLayout(size: size, component: component, transition: .easeInOut(duration: 0.2)) + } + self.released() + default: + break + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func updateLayout(size: CGSize, component: TextSizeSliderComponent, transition: Transition) -> CGSize { + self.component = component + + let previousSize = self.validSize + self.validSize = size + + if self.backgroundNode.view.superview == nil { + self.addSubview(self.backgroundNode.view) + } + if self.knobContainer.superlayer == nil { + self.layer.addSublayer(self.knobContainer) + } + if self.knob.superlayer == nil { + self.knob.contents = generateKnobImage()?.cgImage + self.knobContainer.addSublayer(self.knob) + } + + let isTracking = self.isPanning || self.isPressing + if self.isTracking != isTracking { + self.isTracking = isTracking + transition.setSublayerTransform(view: self, transform: isTracking ? CATransform3DMakeTranslation(8.0, 0.0, 0.0) : CATransform3DMakeTranslation(-size.width / 2.0, 0.0, 0.0)) + transition.setSublayerTransform(layer: self.knobContainer, transform: isTracking ? CATransform3DIdentity : CATransform3DMakeTranslation(4.0, 0.0, 0.0)) + } + + let knobTransition = self.isPanning ? transition.withAnimation(.none) : transition + let knobSize = CGSize(width: 52.0, height: 52.0) + let knobFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - knobSize.width) / 2.0), y: -12.0 + floorToScreenPixels((size.height + 24.0 - knobSize.height) * (1.0 - component.value))), size: knobSize) + knobTransition.setFrame(layer: self.knob, frame: knobFrame) + + transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.knobContainer, frame: CGRect(origin: CGPoint(), size: size)) + + if previousSize != size { + transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: size)) + self.maskLayer.path = generateMaskPath(size: size, topRadius: 15.0, bottomRadius: 3.0).cgPath + self.backgroundNode.layer.mask = self.maskLayer + } + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + view.updated = self.updated + view.released = self.released + return view.updateLayout(size: availableSize, component: self, transition: transition) + } +} diff --git a/submodules/DrawingUI/Sources/ToolsComponent.swift b/submodules/DrawingUI/Sources/ToolsComponent.swift new file mode 100644 index 00000000000..36cbfdeedf8 --- /dev/null +++ b/submodules/DrawingUI/Sources/ToolsComponent.swift @@ -0,0 +1,534 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import LegacyComponents +import TelegramCore +import Postbox + +private let toolSize = CGSize(width: 40.0, height: 176.0) + +private class ToolView: UIView, UIGestureRecognizerDelegate { + let type: DrawingToolState.Key + + var isSelected = false + var isToolFocused = false + var isVisible = false + private var currentSize: CGFloat? + + private let shadow: SimpleLayer + private let tip: UIImageView + private let background: SimpleLayer + private let band: SimpleGradientLayer + + var pressed: (DrawingToolState.Key) -> Void = { _ in } + var swiped: (DrawingToolState.Key, CGFloat) -> Void = { _, _ in } + var released: () -> Void = { } + + init(type: DrawingToolState.Key) { + self.type = type + self.shadow = SimpleLayer() + + self.tip = UIImageView() + self.tip.isUserInteractionEnabled = false + + self.background = SimpleLayer() + + self.band = SimpleGradientLayer() + self.band.cornerRadius = 2.0 + self.band.type = .axial + self.band.startPoint = CGPoint(x: 0.0, y: 0.5) + self.band.endPoint = CGPoint(x: 1.0, y: 0.5) + self.band.masksToBounds = true + + let backgroundImage: UIImage? + let tipImage: UIImage? + let shadowImage: UIImage? + + var tipAbove = true + var hasBand = true + + switch type { + case .pen: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolPen") + tipImage = UIImage(bundleImageName: "Media Editor/ToolPenTip")?.withRenderingMode(.alwaysTemplate) + shadowImage = UIImage(bundleImageName: "Media Editor/ToolPenShadow") + case .arrow: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolArrow") + tipImage = UIImage(bundleImageName: "Media Editor/ToolArrowTip")?.withRenderingMode(.alwaysTemplate) + shadowImage = UIImage(bundleImageName: "Media Editor/ToolArrowShadow") + case .marker: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolMarker") + tipImage = UIImage(bundleImageName: "Media Editor/ToolMarkerTip")?.withRenderingMode(.alwaysTemplate) + tipAbove = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolMarkerShadow") + case .neon: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolNeon") + tipImage = UIImage(bundleImageName: "Media Editor/ToolNeonTip")?.withRenderingMode(.alwaysTemplate) + tipAbove = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolNeonShadow") + case .eraser: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolEraser") + tipImage = nil + hasBand = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolEraserShadow") + case .blur: + backgroundImage = UIImage(bundleImageName: "Media Editor/ToolBlur") + tipImage = UIImage(bundleImageName: "Media Editor/ToolBlurTip") + tipAbove = false + hasBand = false + shadowImage = UIImage(bundleImageName: "Media Editor/ToolBlurShadow") + } + + self.tip.image = tipImage + self.background.contents = backgroundImage?.cgImage + self.shadow.contents = shadowImage?.cgImage + + super.init(frame: CGRect(origin: .zero, size: toolSize)) + + self.tip.frame = CGRect(origin: .zero, size: toolSize) + self.shadow.frame = CGRect(origin: .zero, size: toolSize).insetBy(dx: -4.0, dy: 0.0) + self.background.frame = CGRect(origin: .zero, size: toolSize) + + self.band.frame = CGRect(origin: CGPoint(x: 3.0, y: 64.0), size: CGSize(width: toolSize.width - 6.0, height: toolSize.width - 16.0)) + self.band.anchorPoint = CGPoint(x: 0.5, y: 0.0) + + self.layer.addSublayer(self.shadow) + + if tipAbove { + self.layer.addSublayer(self.background) + self.addSubview(self.tip) + } else { + self.addSubview(self.tip) + self.layer.addSublayer(self.background) + } + + if hasBand { + self.layer.addSublayer(self.band) + } + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer { + if self.isSelected { + return true + } else { + return false + } + } + return self.isVisible + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.pressed(self.type) + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let size = self.currentSize else { + return + } + switch gestureRecognizer.state { + case .changed: + let translation = gestureRecognizer.translation(in: self) + gestureRecognizer.setTranslation(.zero, in: self) + + let updatedSize = max(0.0, min(1.0, size - translation.y / 200.0)) + self.swiped(self.type, updatedSize) + case .ended, .cancelled: + self.released() + default: + break + } + } + + func animateIn(animated: Bool, delay: Double = 0.0) { + let layout = { + self.bounds = CGRect(origin: .zero, size: self.bounds.size) + } + if animated { + UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout) + } else { + layout() + } + } + + func animateOut(animated: Bool, delay: Double = 0.0, completion: @escaping () -> Void = {}) { + let layout = { + self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -140.0), size: self.bounds.size) + } + if animated { + UIView.animate(withDuration: 0.5, delay: delay, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, animations: layout, completion: { _ in + completion() + }) + } else { + layout() + completion() + } + } + + func update(state: DrawingToolState) { + self.currentSize = state.size + + if let _ = self.tip.image { + let color = state.color?.toUIColor() + self.tip.tintColor = color + + guard let color = color else { + return + } + var locations: [NSNumber] = [0.0, 1.0] + var colors: [CGColor] = [] + switch self.type { + case .pen, .arrow: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + case .marker: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + case .neon: + locations = [0.0, 0.15, 0.85, 1.0] + colors = [ + color.withMultipliedBrightnessBy(0.7).cgColor, + color.cgColor, + color.cgColor, + color.withMultipliedBrightnessBy(0.7).cgColor + ] + default: + return + } + + self.band.transform = CATransform3DMakeScale(1.0, 0.08 + 0.92 * (state.size ?? 1.0), 1.0) + + self.band.locations = locations + self.band.colors = colors + } + } +} + +final class ToolsComponent: Component { + let state: DrawingState + let isFocused: Bool + let tag: AnyObject? + let toolPressed: (DrawingToolState.Key) -> Void + let toolResized: (DrawingToolState.Key, CGFloat) -> Void + let sizeReleased: () -> Void + + init(state: DrawingState, isFocused: Bool, tag: AnyObject?, toolPressed: @escaping (DrawingToolState.Key) -> Void, toolResized: @escaping (DrawingToolState.Key, CGFloat) -> Void, sizeReleased: @escaping () -> Void) { + self.state = state + self.isFocused = isFocused + self.tag = tag + self.toolPressed = toolPressed + self.toolResized = toolResized + self.sizeReleased = sizeReleased + } + + static func == (lhs: ToolsComponent, rhs: ToolsComponent) -> Bool { + return lhs.state == rhs.state && lhs.isFocused == rhs.isFocused + } + + public final class View: UIView, ComponentTaggedView { + private var toolViews: [ToolView] = [] + private let maskImageView: UIImageView + + private var isToolFocused: Bool? + + private var component: ToolsComponent? + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + override init(frame: CGRect) { + self.maskImageView = UIImageView() + self.maskImageView.image = generateGradientImage(size: CGSize(width: 1.0, height: 120.0), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.88, 1.0], direction: .vertical) + + super.init(frame: frame) + + self.mask = self.maskImageView + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self { + return nil + } + return result + } + + func animateIn(completion: @escaping () -> Void) { + var delay = 0.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + view.animateOut(animated: false) + view.animateIn(animated: true, delay: delay) + delay += 0.025 + } + } + + func animateOut(completion: @escaping () -> Void) { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + var delay = 0.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + view.animateOut(animated: true, delay: delay, completion: i == self.toolViews.count - 1 ? completion : {}) + delay += 0.025 + + transition.setPosition(view: view, position: CGPoint(x: view.center.x, y: toolSize.height / 2.0 - 30.0 + 34.0)) + } + } + + func update(component: ToolsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + if self.toolViews.isEmpty { + var toolViews: [ToolView] = [] + for type in DrawingToolState.Key.allCases { + if component.state.tools.contains(where: { $0.key == type }) { + let toolView = ToolView(type: type) + toolViews.append(toolView) + self.addSubview(toolView) + } + } + self.toolViews = toolViews + } + + let wasFocused = self.isToolFocused + + self.isToolFocused = component.isFocused + + let toolPressed = component.toolPressed + let toolResized = component.toolResized + let toolSizeReleased = component.sizeReleased + + let spacing: CGFloat = 44.0 + let totalWidth = spacing * CGFloat(self.toolViews.count - 1) + + let left = (availableSize.width - totalWidth) / 2.0 + var xPositions: [CGFloat] = [] + + var selectedIndex = 0 + let isFocused = component.isFocused + + for i in 0 ..< self.toolViews.count { + xPositions.append(left + spacing * CGFloat(i)) + + if self.toolViews[i].type == component.state.selectedTool { + selectedIndex = i + } + } + + if isFocused { + let originalFocusedToolPosition = xPositions[selectedIndex] + xPositions[selectedIndex] = availableSize.width / 2.0 + + let delta = availableSize.width / 2.0 - originalFocusedToolPosition + + for i in 0 ..< xPositions.count { + if i != selectedIndex { + xPositions[i] += delta + } + } + } + + var offset: CGFloat = 100.0 + for i in 0 ..< self.toolViews.count { + let view = self.toolViews[i] + + var scale = 0.5 + var verticalOffset: CGFloat = 30.0 + if i == selectedIndex { + if isFocused { + scale = 1.0 + verticalOffset = 30.0 + } else { + verticalOffset = 18.0 + } + view.isSelected = true + view.isToolFocused = isFocused + view.isVisible = true + } else { + view.isSelected = false + view.isToolFocused = false + view.isVisible = !isFocused + } + view.isUserInteractionEnabled = view.isVisible + + let layout = { + view.center = CGPoint(x: xPositions[i], y: toolSize.height / 2.0 - 30.0 + verticalOffset) + view.transform = CGAffineTransform(scaleX: scale, y: scale) + } + if case .curve = transition.animation { + UIView.animate( + withDuration: 0.7, + delay: 0.0, + usingSpringWithDamping: 0.6, + initialSpringVelocity: 0.0, + options: .allowUserInteraction, + animations: layout) + } else { + layout() + } + + view.update(state: component.state.toolState(for: view.type)) + + view.pressed = { type in + toolPressed(type) + } + view.swiped = { type, size in + toolResized(type, size) + } + view.released = { + toolSizeReleased() + } + + offset += 44.0 + } + + + if wasFocused != nil && wasFocused != component.isFocused { + var animated = false + if case .curve = transition.animation { + animated = true + } + if isFocused { + var delay = 0.0 + for i in (selectedIndex + 1 ..< self.toolViews.count).reversed() { + let view = self.toolViews[i] + view.animateOut(animated: animated, delay: delay) + delay += 0.025 + } + delay = 0.0 + for i in (0 ..< selectedIndex) { + let view = self.toolViews[i] + view.animateOut(animated: animated, delay: delay) + delay += 0.025 + } + } else { + var delay = 0.0 + for i in (selectedIndex + 1 ..< self.toolViews.count) { + let view = self.toolViews[i] + view.animateIn(animated: animated, delay: delay) + delay += 0.025 + } + delay = 0.0 + for i in (0 ..< selectedIndex).reversed() { + let view = self.toolViews[i] + view.animateIn(animated: animated, delay: delay) + delay += 0.025 + } + } + } + + self.maskImageView.frame = CGRect(origin: .zero, size: availableSize) + + if let screenTransition = transition.userData(DrawingScreenTransition.self) { + switch screenTransition { + case .animateIn: + self.animateIn(completion: {}) + case .animateOut: + self.animateOut(completion: {}) + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class ZoomOutButtonContent: CombinedComponent { + let title: String + let image: UIImage + + init( + title: String, + image: UIImage + ) { + self.title = title + self.image = image + } + + static func ==(lhs: ZoomOutButtonContent, rhs: ZoomOutButtonContent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.image !== rhs.image { + return false + } + return true + } + + static var body: Body { + let title = Child(Text.self) + let image = Child(Image.self) + + return { context in + let component = context.component + + let title = title.update( + component: Text( + text: component.title, + font: Font.regular(17.0), + color: .white + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let image = image.update( + component: Image(image: component.image), + availableSize: CGSize(width: 24.0, height: 24.0), + transition: .immediate + ) + + let spacing: CGFloat = 2.0 + let width = title.size.width + spacing + image.size.width + context.add(image + .position(CGPoint(x: image.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + context.add(title + .position(CGPoint(x: image.size.width + spacing + title.size.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return CGSize(width: width, height: context.availableSize.height) + } + } +} diff --git a/submodules/DrawingUI/Sources/Unistroke.swift b/submodules/DrawingUI/Sources/Unistroke.swift new file mode 100644 index 00000000000..f73219e8b1e --- /dev/null +++ b/submodules/DrawingUI/Sources/Unistroke.swift @@ -0,0 +1,241 @@ +import Foundation +import UIKit + +private let pointsCount: Int = 64 +private let squareSize: Double = 250.0 +private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize) +private let halfDiagonal = diagonal * 0.5 +private let angleRange: Double = .pi / 4.0 +private let anglePrecision: Double = .pi / 90.0 + +class Unistroke { + let points: [CGPoint] + + init(points: [CGPoint]) { + var points = resample(points: points, totalPoints: pointsCount) + let radians = indicativeAngle(points: points) + points = rotate(points: points, byRadians: -radians) + points = scale(points: points, toSize: squareSize) + points = translate(points: points, to: .zero) + self.points = points + } + + func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? { + var bestDistance = Double.infinity + var bestTemplate: UnistrokeTemplate? + for template in templates { + let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision) + if templateDistance < bestDistance { + bestDistance = templateDistance + bestTemplate = template + } + } + + if let bestTemplate = bestTemplate { + bestDistance = 1.0 - bestDistance / halfDiagonal + if bestDistance < minThreshold { + return nil + } + return bestTemplate.name + } else { + return nil + } + } +} + +class UnistrokeTemplate : Unistroke { + var name: String + + init(name: String, points: [CGPoint]) { + self.name = name + super.init(points: points) + } +} + +private struct Edge { + var minX: Double + var minY: Double + var maxX: Double + var maxY: Double + + init(minX: Double, maxX: Double, minY: Double, maxY: Double) { + self.minX = minX + self.minY = minY + self.maxX = maxX + self.maxY = maxY + } + + mutating func addPoint(value: CGPoint) { + self.minX = min(self.minX,value.x) + self.maxX = max(self.maxX,value.x) + self.minY = min(self.minY,value.y) + self.maxY = max(self.maxY,value.y) + } + +} + +private extension Double { + func toRadians() -> Double { + let res = self * .pi / 180.0 + return res + } +} + +private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] { + var initialPoints = points + let interval = pathLength(points: initialPoints) / Double(totalPoints - 1) + var totalLength: Double = 0.0 + var newPoints: [CGPoint] = [points[0]] + for i in 1 ..< initialPoints.count { + let currentLength = initialPoints[i - 1].distance(to: initialPoints[i]) + if totalLength + currentLength >= interval { + let newPoint = CGPoint( + x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x), + y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y) + ) + newPoints.append(newPoint) + initialPoints.insert(newPoint, at: i) + totalLength = 0.0 + } else { + totalLength += currentLength + } + } + if newPoints.count == totalPoints - 1 { + newPoints.append(points.last!) + } + return newPoints +} + +private func pathLength(points: [CGPoint]) -> Double { + var distance: Double = 0.0 + for index in 1 ..< points.count { + distance += points[index - 1].distance(to: points[index]) + } + return distance +} + +private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double { + var d: Double = 0.0 + for idx in 0 ..< min(path1.count, path2.count) { + d += path1[idx].distance(to: path2[idx]) + } + return d / Double(path1.count) +} + +private func centroid(points: [CGPoint]) -> CGPoint { + var centroidPoint: CGPoint = .zero + for point in points { + centroidPoint.x = centroidPoint.x + point.x + centroidPoint.y = centroidPoint.y + point.y + } + centroidPoint.x = (centroidPoint.x / Double(points.count)) + centroidPoint.y = (centroidPoint.y / Double(points.count)) + return centroidPoint +} + +private func boundingBox(points: [CGPoint]) -> CGRect { + var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity) + for point in points { + edge.addPoint(value: point) + } + return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY)) +} + +private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] { + let centroid = centroid(points: points) + let cosinus = cos(radians) + let sinus = sin(radians) + var result: [CGPoint] = [] + for point in points { + result.append( + CGPoint( + x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x, + y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y + ) + ) + } + return result +} + +private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] { + let boundingBox = boundingBox(points: points) + var result: [CGPoint] = [] + for point in points { + result.append( + CGPoint( + x: point.x * (size / boundingBox.width), + y: point.y * (size / boundingBox.height) + ) + ) + } + return result +} + +private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] { + let centroidPoint = centroid(points: points) + var newPoints: [CGPoint] = [] + for point in points { + newPoints.append( + CGPoint( + x: point.x + pt.x - centroidPoint.x, + y: point.y + pt.y - centroidPoint.y + ) + ) + } + return newPoints +} + +private func vectorize(points: [CGPoint]) -> [Double] { + var sum: Double = 0.0 + var vector: [Double] = [] + for point in points { + vector.append(point.x) + vector.append(point.y) + sum += (point.x * point.x) + (point.y * point.y) + } + let magnitude = sqrt(sum) + for i in 0 ..< vector.count { + vector[i] = vector[i] / magnitude + } + return vector +} + +private func indicativeAngle(points: [CGPoint]) -> Double { + let centroid = centroid(points: points) + return atan2(centroid.y - points[0].y, centroid.x - points[0].x) +} + +private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double { + func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double { + let rotatedPoints = rotate(points: points, byRadians: radians) + return pathDistance(path1: rotatedPoints, path2: strokeTemplate) + } + + let phi: Double = (0.5 * (-1.0 + sqrt(5.0))) + + var fromAngle = fromAngle + var toAngle = toAngle + + var x1 = phi * fromAngle + (1.0 - phi) * toAngle + var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1) + + var x2 = (1.0 - phi) * fromAngle + phi * toAngle + var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2) + + while abs(toAngle - fromAngle) > threshold { + if f1 < f2 { + toAngle = x2 + x2 = x1 + f2 = f1 + x1 = phi * fromAngle + (1.0 - phi) * toAngle + f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1) + } else { + fromAngle = x1 + x1 = x2 + f1 = f2 + x2 = (1.0 - phi) * fromAngle + phi * toAngle + f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2) + } + } + return min(f1, f2) +} diff --git a/submodules/FeaturedStickersScreen/BUILD b/submodules/FeaturedStickersScreen/BUILD new file mode 100644 index 00000000000..6ccb1389189 --- /dev/null +++ b/submodules/FeaturedStickersScreen/BUILD @@ -0,0 +1,39 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "FeaturedStickersScreen", + module_name = "FeaturedStickersScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/MergeLists:MergeLists", + "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", + "//submodules/StickerPeekUI:StickerPeekUI", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/SearchBarNode:SearchBarNode", + "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", + "//submodules/PremiumUI:PremiumUI", + "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", + "//submodules/StickerResources:StickerResources", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/FeaturedStickersScreen/Sources/ChatMediaInputPane.swift b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputPane.swift new file mode 100644 index 00000000000..e94f4fe60f3 --- /dev/null +++ b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputPane.swift @@ -0,0 +1,19 @@ +import Foundation +import AsyncDisplayKit +import Display +import ChatPresentationInterfaceState +import TelegramPresentationData + +open class ChatMediaInputPane: ASDisplayNode { + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var collectionListPanelOffset: CGFloat = 0.0 + var isEmpty: Bool { + return false + } + + open func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + } + + open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + } +} diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift similarity index 88% rename from submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift rename to submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift index f4369e9256a..02e207b48a3 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift @@ -12,15 +12,16 @@ import AccountContext import StickerPackPreviewUI import PresentationDataUtils import UndoUI +import ChatControllerInteraction -final class TrendingPaneInteraction { - let installPack: (ItemCollectionInfo) -> Void - let openPack: (ItemCollectionInfo) -> Void - let getItemIsPreviewed: (StickerPackItem) -> Bool - let openSearch: () -> Void - let itemContext = StickerPaneSearchGlobalItemContext() +public final class TrendingPaneInteraction { + public let installPack: (ItemCollectionInfo) -> Void + public let openPack: (ItemCollectionInfo) -> Void + public let getItemIsPreviewed: (StickerPackItem) -> Bool + public let openSearch: () -> Void + public let itemContext = StickerPaneSearchGlobalItemContext() - init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) { + public init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) { self.installPack = installPack self.openPack = openPack self.getItemIsPreviewed = getItemIsPreviewed @@ -28,17 +29,17 @@ final class TrendingPaneInteraction { } } -final class TrendingPanePackEntry: Identifiable, Comparable { - let index: Int - let info: StickerPackCollectionInfo - let theme: PresentationTheme - let strings: PresentationStrings - let topItems: [StickerPackItem] - let installed: Bool - let unread: Bool - let topSeparator: Bool +public final class TrendingPanePackEntry: Identifiable, Comparable { + public let index: Int + public let info: StickerPackCollectionInfo + public let theme: PresentationTheme + public let strings: PresentationStrings + public let topItems: [StickerPackItem] + public let installed: Bool + public let unread: Bool + public let topSeparator: Bool - init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) { + public init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) { self.index = index self.info = info self.theme = theme @@ -49,11 +50,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable { self.topSeparator = topSeparator } - var stableId: ItemCollectionId { + public var stableId: ItemCollectionId { return self.info.id } - static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { + public static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { if lhs.index != rhs.index { return false } @@ -81,11 +82,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable { return true } - static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { + public static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem { + public func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem { let info = self.info return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, listAppearance: false, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: false, installed: self.installed, unread: self.unread, open: { interaction.openPack(info) @@ -190,13 +191,13 @@ private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], ins return result } -final class ChatMediaInputTrendingPane: ChatMediaInputPane { +public final class ChatMediaInputTrendingPane: ChatMediaInputPane { private let context: AccountContext private let controllerInteraction: ChatControllerInteraction private let getItemIsPreviewed: (StickerPackItem) -> Bool private let isPane: Bool - let gridNode: GridNode + public let gridNode: GridNode private var enqueuedTransitions: [TrendingPaneTransition] = [] private var validLayout: (CGSize, CGFloat)? @@ -206,15 +207,15 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { private let _ready = Promise() private var didSetReady = false - var ready: Signal { + public var ready: Signal { return self._ready.get() } - var scrollingInitiated: (() -> Void)? + public var scrollingInitiated: (() -> Void)? private let installDisposable = MetaDisposable() - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) { + public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) { self.context = context self.controllerInteraction = controllerInteraction self.getItemIsPreviewed = getItemIsPreviewed @@ -236,7 +237,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { self.installDisposable.dispose() } - func activate() { + public func activate() { if self.isActivated { return } @@ -369,7 +370,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { }) } - override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + public override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { let hadValidLayout = self.validLayout != nil self.validLayout = (size, bottomInset) @@ -401,7 +402,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } } - override func willEnterHierarchy() { + public override func willEnterHierarchy() { super.willEnterHierarchy() self.activate() @@ -416,7 +417,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } } - func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { let localPoint = self.view.convert(point, to: self.gridNode.view) var resultNode: StickerPaneSearchGlobalItemNode? self.gridNode.forEachItemNode { itemNode in @@ -430,7 +431,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { return nil } - func updatePreviewing(animated: Bool) { + public func updatePreviewing(animated: Bool) { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { itemNode.updatePreviewing(animated: animated) diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift similarity index 98% rename from submodules/TelegramUI/Sources/FeaturedStickersScreen.swift rename to submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index 1938a1364ac..94985ccb08c 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -17,6 +17,7 @@ import SearchBarNode import UndoUI import ContextUI import PremiumUI +import ChatPresentationInterfaceState private final class FeaturedInteraction { let installPack: (ItemCollectionInfo, Bool) -> Void @@ -787,7 +788,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } } -final class FeaturedStickersScreen: ViewController { +public final class FeaturedStickersScreen: ViewController { private let context: AccountContext fileprivate let highlightedPackId: ItemCollectionId? private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? @@ -1492,3 +1493,17 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } } } + +public final class StickerPaneSearchInteraction { + public let open: (StickerPackCollectionInfo) -> Void + public let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void + public let sendSticker: (FileMediaReference, UIView, CGRect) -> Void + public let getItemIsPreviewed: (StickerPackItem) -> Bool + + public init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + self.open = open + self.install = install + self.sendSticker = sendSticker + self.getItemIsPreviewed = getItemIsPreviewed + } +} diff --git a/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift b/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift similarity index 91% rename from submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift rename to submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift index e16263b237f..871c3aee3ca 100644 --- a/submodules/TelegramUI/Sources/MediaInputPaneTrendingItem.swift +++ b/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift @@ -134,23 +134,23 @@ final class TrendingTopItemNode: ASDisplayNode { let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) if item.file.isVideoSticker { - self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, file: item.file, small: false, synchronousLoad: synchronousLoads)) + self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, userLocation: .other, file: item.file, small: false, synchronousLoad: synchronousLoads)) } else { - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: false, size: fittedDimensions, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, userLocation: .other, file: item.file, small: false, size: fittedDimensions, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads) } animationNode.started = { [weak self] in self?.imageNode.alpha = 0.0 } animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) - self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) + self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) } else { - self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads) if let currentAnimationNode = self.animationNode { self.animationNode = nil currentAnimationNode.removeFromSupernode() } - self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start()) + self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start()) } } diff --git a/submodules/TelegramUI/Sources/PaneSearchBarPlaceholderItem.swift b/submodules/FeaturedStickersScreen/Sources/PaneSearchBarPlaceholderItem.swift similarity index 79% rename from submodules/TelegramUI/Sources/PaneSearchBarPlaceholderItem.swift rename to submodules/FeaturedStickersScreen/Sources/PaneSearchBarPlaceholderItem.swift index a3277a78c01..7e5c08cacf3 100644 --- a/submodules/TelegramUI/Sources/PaneSearchBarPlaceholderItem.swift +++ b/submodules/FeaturedStickersScreen/Sources/PaneSearchBarPlaceholderItem.swift @@ -11,35 +11,35 @@ private func generateLoupeIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: templateLoupeIcon, color: color) } -enum PaneSearchBarType { +public enum PaneSearchBarType { case stickers case gifs } -final class PaneSearchBarPlaceholderItem: GridItem { - let theme: PresentationTheme - let strings: PresentationStrings - let type: PaneSearchBarType - let activate: () -> Void +public final class PaneSearchBarPlaceholderItem: GridItem { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let type: PaneSearchBarType + public let activate: () -> Void - let section: GridSection? = nil - let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true) + public let section: GridSection? = nil + public let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true) - init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) { + public init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) { self.theme = theme self.strings = strings self.type = type self.activate = activate } - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = PaneSearchBarPlaceholderNode() node.activate = self.activate node.setup(theme: self.theme, strings: self.strings, type: self.type) return node } - func update(node: GridItemNode) { + public func update(node: GridItemNode) { guard let node = node as? PaneSearchBarPlaceholderNode else { assertionFailure() return @@ -49,15 +49,15 @@ final class PaneSearchBarPlaceholderItem: GridItem { } } -final class PaneSearchBarPlaceholderNode: GridItemNode { +public final class PaneSearchBarPlaceholderNode: GridItemNode { private var currentState: (PresentationTheme, PresentationStrings, PaneSearchBarType)? - var activate: (() -> Void)? + public var activate: (() -> Void)? - let backgroundNode: ASImageNode - let labelNode: ImmediateTextNode - let iconNode: ASImageNode + public let backgroundNode: ASImageNode + public let labelNode: ImmediateTextNode + public let iconNode: ASImageNode - override init() { + public override init() { self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true @@ -82,13 +82,13 @@ final class PaneSearchBarPlaceholderNode: GridItemNode { self.addSubnode(self.iconNode) } - override func didLoad() { + public override func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) { + public func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) { if self.currentState?.0 !== theme || self.currentState?.1 !== strings || self.currentState?.2 != type { self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor) self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor) @@ -105,7 +105,7 @@ final class PaneSearchBarPlaceholderNode: GridItemNode { } } - override func layout() { + public override func layout() { super.layout() let bounds = self.bounds diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift similarity index 90% rename from submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift rename to submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift index ee041b7f047..22083627916 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchGlobaltem.swift +++ b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift @@ -69,30 +69,34 @@ private final class StickerPaneSearchGlobalSectionNode: ASDisplayNode { } } -final class StickerPaneSearchGlobalItemContext { - var canPlayMedia: Bool = false +public final class StickerPaneSearchGlobalItemContext { + public var canPlayMedia: Bool + + public init(canPlayMedia: Bool = false) { + self.canPlayMedia = canPlayMedia + } } -final class StickerPaneSearchGlobalItem: GridItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings - let listAppearance: Bool - let fillsRow: Bool - let info: StickerPackCollectionInfo - let topItems: [StickerPackItem] - let topSeparator: Bool - let regularInsets: Bool - let installed: Bool - let installing: Bool - let unread: Bool - let open: () -> Void - let install: () -> Void - let getItemIsPreviewed: (StickerPackItem) -> Bool - let itemContext: StickerPaneSearchGlobalItemContext - - let section: GridSection? - var fillsRowWithHeight: (CGFloat, Bool)? { +public final class StickerPaneSearchGlobalItem: GridItem { + public let account: Account + public let theme: PresentationTheme + public let strings: PresentationStrings + public let listAppearance: Bool + public let fillsRow: Bool + public let info: StickerPackCollectionInfo + public let topItems: [StickerPackItem] + public let topSeparator: Bool + public let regularInsets: Bool + public let installed: Bool + public let installing: Bool + public let unread: Bool + public let open: () -> Void + public let install: () -> Void + public let getItemIsPreviewed: (StickerPackItem) -> Bool + public let itemContext: StickerPaneSearchGlobalItemContext + + public let section: GridSection? + public var fillsRowWithHeight: (CGFloat, Bool)? { var additionalHeight: CGFloat = 0.0 if self.regularInsets { additionalHeight = 12.0 + 12.0 @@ -106,7 +110,7 @@ final class StickerPaneSearchGlobalItem: GridItem { return (128.0 + additionalHeight, self.fillsRow) } - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) { + public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) { self.account = account self.theme = theme self.strings = strings @@ -126,13 +130,13 @@ final class StickerPaneSearchGlobalItem: GridItem { self.section = StickerPaneSearchGlobalSection(title: sectionTitle, theme: theme) } - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = StickerPaneSearchGlobalItemNode() node.setup(item: self) return node } - func update(node: GridItemNode) { + public func update(node: GridItemNode) { guard let node = node as? StickerPaneSearchGlobalItemNode else { assertionFailure() return @@ -145,7 +149,7 @@ private let titleFont = Font.bold(16.0) private let statusFont = Font.regular(15.0) private let buttonFont = Font.semibold(13.0) -class StickerPaneSearchGlobalItemNode: GridItemNode { +public class StickerPaneSearchGlobalItemNode: GridItemNode { private let titleNode: TextNode private let descriptionNode: TextNode private let unreadNode: ASImageNode @@ -159,7 +163,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { private let topSeparatorNode: ASDisplayNode private var highlightNode: ASDisplayNode? - var item: StickerPaneSearchGlobalItem? + public var item: StickerPaneSearchGlobalItem? private var appliedItem: StickerPaneSearchGlobalItem? private let preloadDisposable = MetaDisposable() private let preloadedStickerPackThumbnailDisposable = MetaDisposable() @@ -175,7 +179,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } - override var isVisibleInGrid: Bool { + public override var isVisibleInGrid: Bool { didSet { if oldValue != self.isVisibleInGrid { self.updatePlayback() @@ -200,7 +204,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } - override init() { + public override init() { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left @@ -298,14 +302,14 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.preloadedStickerPackThumbnailDisposable.dispose() } - override func didLoad() { + public override func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } private var absoluteLocation: (CGRect, CGSize)? - override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + public override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteLocation = (rect, containerSize) for node in self.itemNodes { @@ -314,7 +318,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } - func setup(item: StickerPaneSearchGlobalItem) { + public func setup(item: StickerPaneSearchGlobalItem) { if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && self.item?.info.id != item.info.id { self.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.account.network, postbox: item.account.postbox, id: item.info.id).start()) } @@ -325,7 +329,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.updatePreviewing(animated: false) } - func updateCanPlayMedia() { + public func updateCanPlayMedia() { guard let item = self.item else { return } @@ -333,7 +337,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { self.canPlayMedia = item.itemContext.canPlayMedia } - func highlight() { + public func highlight() { guard self.highlightNode == nil else { return } @@ -354,7 +358,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } - override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { + public override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { guard let item = self.item else { return } @@ -521,7 +525,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { } } - func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { + public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { guard let item = self.item else { return nil } @@ -535,7 +539,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode { return nil } - func updatePreviewing(animated: Bool) { + public func updatePreviewing(animated: Bool) { guard let item = self.item else { return } diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift similarity index 81% rename from submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift rename to submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift index 158cb2b2a41..5fb72f5320b 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchStickerItem.swift +++ b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift @@ -10,6 +10,7 @@ import StickerResources import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode +import ChatPresentationInterfaceState final class StickerPaneSearchStickerSection: GridSection { let code: String @@ -65,16 +66,16 @@ final class StickerPaneSearchStickerSectionNode: ASDisplayNode { } } -final class StickerPaneSearchStickerItem: GridItem { - let account: Account - let code: String? - let stickerItem: FoundStickerItem - let selected: (ASDisplayNode, CGRect) -> Void - let inputNodeInteraction: ChatMediaInputNodeInteraction +public final class StickerPaneSearchStickerItem: GridItem { + public let account: Account + public let code: String? + public let stickerItem: FoundStickerItem + public let selected: (ASDisplayNode, CGRect) -> Void + public let inputNodeInteraction: ChatMediaInputNodeInteraction - let section: GridSection? + public let section: GridSection? - init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) { + public init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) { self.account = account self.stickerItem = stickerItem self.inputNodeInteraction = inputNodeInteraction @@ -83,7 +84,7 @@ final class StickerPaneSearchStickerItem: GridItem { self.section = nil } - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = StickerPaneSearchStickerItemNode() node.inputNodeInteraction = self.inputNodeInteraction node.setup(account: self.account, stickerItem: self.stickerItem, code: self.code) @@ -91,7 +92,7 @@ final class StickerPaneSearchStickerItem: GridItem { return node } - func update(node: GridItemNode) { + public func update(node: GridItemNode) { guard let node = node as? StickerPaneSearchStickerItemNode else { assertionFailure() return @@ -104,17 +105,17 @@ final class StickerPaneSearchStickerItem: GridItem { private let textFont = Font.regular(20.0) -final class StickerPaneSearchStickerItemNode: GridItemNode { +public final class StickerPaneSearchStickerItemNode: GridItemNode { private var currentState: (Account, FoundStickerItem, CGSize)? - let imageNode: TransformImageNode - private(set) var animationNode: AnimatedStickerNode? + public let imageNode: TransformImageNode + public private(set) var animationNode: AnimatedStickerNode? private let textNode: ASTextNode private let stickerFetchedDisposable = MetaDisposable() - var currentIsPreviewing = false + public var currentIsPreviewing = false - override var isVisibleInGrid: Bool { + public override var isVisibleInGrid: Bool { didSet { self.updateVisibility() } @@ -122,14 +123,14 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { private var isPlaying = false - var inputNodeInteraction: ChatMediaInputNodeInteraction? - var selected: ((ASDisplayNode, CGRect) -> Void)? + public var inputNodeInteraction: ChatMediaInputNodeInteraction? + public var selected: ((ASDisplayNode, CGRect) -> Void)? - var stickerItem: FoundStickerItem? { + public var stickerItem: FoundStickerItem? { return self.currentState?.1 } - override init() { + public override init() { self.imageNode = TransformImageNode() self.textNode = ASTextNode() self.textNode.isUserInteractionEnabled = false @@ -145,7 +146,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { self.stickerFetchedDisposable.dispose() } - override func didLoad() { + public override func didLoad() { super.didLoad() self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) @@ -167,15 +168,15 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource, isVideo: stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) self.animationNode?.visibility = self.isVisibleInGrid - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) } else { if let animationNode = self.animationNode { animationNode.visibility = false self.animationNode = nil animationNode.removeFromSupernode() } - self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) } self.currentState = (account, stickerItem, dimensions.cgSize) @@ -184,7 +185,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { } } - override func layout() { + public override func layout() { super.layout() let bounds = self.bounds @@ -211,11 +212,11 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { self.selected?(self, self.bounds) } - func transitionNode() -> ASDisplayNode? { + public func transitionNode() -> ASDisplayNode? { return self.imageNode } - func updateVisibility() { + public func updateVisibility() { let isPlaying = self.isVisibleInGrid if self.isPlaying != isPlaying { self.isPlaying = isPlaying @@ -223,7 +224,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { } } - func updatePreviewing(animated: Bool) { + public func updatePreviewing(animated: Bool) { var isPreviewing = false if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction { isPreviewing = interaction.previewedStickerPackItem == .found(item) diff --git a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift index 6597e505294..ad1bfb1feaf 100644 --- a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift +++ b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift @@ -246,7 +246,27 @@ private final class FetchManagerCategoryContext { activeContext.disposable?.dispose() let postbox = self.postbox Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") - activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) + + var userLocation: MediaResourceUserLocation = .other + switch entry.id.location { + case let .chat(peerId): + userLocation = .peer(peerId) + } + var userContentType: MediaResourceUserContentType = .other + switch entry.statsCategory { + case .image: + userContentType = .image + case .video: + userContentType = .video + case .audio: + userContentType = .audio + case .file: + userContentType = .file + default: + userContentType = .other + } + + activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) |> mapToSignal { type -> Signal in if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type { let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start() @@ -358,11 +378,30 @@ private final class FetchManagerCategoryContext { if restart { activeContext.ranges = ranges + var userLocation: MediaResourceUserLocation = .other + switch entry.id.location { + case let .chat(peerId): + userLocation = .peer(peerId) + } + var userContentType: MediaResourceUserContentType = .other + switch entry.statsCategory { + case .image: + userContentType = .image + case .video: + userContentType = .video + case .audio: + userContentType = .audio + case .file: + userContentType = .file + default: + userContentType = .other + } + let entryCompleted = self.entryCompleted let storeManager = self.storeManager activeContext.disposable?.dispose() if isVideoPreload { - activeContext.disposable = (preloadVideoResource(postbox: self.postbox, resourceReference: entry.resourceReference, duration: 4.0) + activeContext.disposable = (preloadVideoResource(postbox: self.postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: entry.resourceReference, duration: 4.0) |> castError(FetchResourceError.self) |> map { _ -> FetchResourceSourceType in } |> then(.single(.local)) @@ -373,7 +412,27 @@ private final class FetchManagerCategoryContext { } else { let postbox = self.postbox Logger.shared.log("FetchManager", "Begin fetching \(entry.resourceReference.resource.id.stringRepresentation) ranges: \(String(describing: parsedRanges))") - activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) + + var userLocation: MediaResourceUserLocation = .other + switch entry.id.location { + case let .chat(peerId): + userLocation = .peer(peerId) + } + var userContentType: MediaResourceUserContentType = .other + switch entry.statsCategory { + case .image: + userContentType = .image + case .video: + userContentType = .video + case .audio: + userContentType = .audio + case .file: + userContentType = .file + default: + userContentType = .other + } + + activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) |> mapToSignal { type -> Signal in if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type { let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start() @@ -389,7 +448,7 @@ private final class FetchManagerCategoryContext { let context = accountContext, NGSettings.shouldDownloadVideo, shouldSave { - let _ = (saveToCameraRoll(context: context, postbox: postbox, mediaReference: mediaReference) + let _ = (saveToCameraRoll(context: context, postbox: postbox, userLocation: userLocation, mediaReference: mediaReference) |> deliverOnMainQueue).start(completed: { Queue.mainQueue().after(0.2) { NGToast.showDefaultToast(backgroundColor: UIColor(rgb: 0x474747), image: nil, title: "The video is downloaded to your phone gallery") diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index ab8410856bf..21bde1bc46b 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -100,7 +100,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati var instantPageMedia: (TelegramMediaWebpage, [InstantPageGalleryEntry])? if message.media.isEmpty, let entities = message.textEntitiesAttribute?.entities, entities.count == 1, let firstEntity = entities.first, case let .CustomEmoji(_, fileId) = firstEntity.type, let file = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { for attribute in file.attributes { - if case let .CustomEmoji(_, _, reference) = attribute { + if case let .CustomEmoji(_, _, _, reference) = attribute { if let reference = reference { return .stickerPack(reference) } @@ -114,10 +114,21 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati galleryMedia = fullMedia } else if let action = media as? TelegramMediaAction { switch action.action { - case let .photoUpdated(image): + case let .photoUpdated(image), let .suggestedProfilePhoto(image): if let peer = messageMainPeer(EngineMessage(message)), let image = image { - let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action")]) - let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: .roundRect(15.5), remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in + var isSuggested = false + if case .suggestedProfilePhoto = action.action { + isSuggested = true + } + let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false)]) + + let sourceCorners: AvatarGalleryController.SourceCorners + if case .photoUpdated = action.action { + sourceCorners = .roundRect(15.5) + } else { + sourceCorners = .round + } + let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in }) return .chatAvatars(galleryController, image) @@ -185,7 +196,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati } } - let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, replaceRootController: { [weak navigationController] controller, ready in + let gallery = InstantPageGalleryController(context: context, userLocation: chatLocation?.peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, replaceRootController: { [weak navigationController] controller, ready in if let navigationController = navigationController { navigationController.replaceTopController(controller, animated: false, ready: ready) } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 9cb591dc0de..240f565033c 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -159,12 +159,12 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese if file.isVideo { let content: UniversalVideoContent if file.isAnimated { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) } else { if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) } else { - content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) + content = PlatformVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), content: .file(.message(message: MessageReference(message), media: file)), streamVideo: streamVideos, loopVideo: loopVideos) } } @@ -207,16 +207,16 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese var content: UniversalVideoContent? switch websiteType(of: webpageContent.websiteName) { case .instagram where webpageContent.file != nil && webpageContent.image != nil && webpageContent.file!.isVideo: - content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected()) + content = NativeVideoContent(id: .message(message.stableId, webpageContent.file?.id ?? webpage.webpageId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: webpageContent.file!), imageReference: webpageContent.image.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, enableSound: true, captureProtected: message.isCopyProtected()) default: if let embedUrl = webpageContent.embedUrl, let image = webpageContent.image { if let file = webpageContent.file, file.isVideo { - content = NativeVideoContent(id: .message(message.stableId, file.fileId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) + content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: message.isCopyProtected()) } else if URL(string: embedUrl)?.pathExtension == "mp4" { - content = SystemVideoContent(url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)) + content = SystemVideoContent(userLocation: .peer(message.id.peerId), url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0)) } } - if content == nil, let webEmbedContent = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in + if content == nil, let webEmbedContent = WebEmbedVideoContent(userLocation: .peer(message.id.peerId), webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in performAction(.url(url: url.absoluteString, concealed: false)) }) { content = webEmbedContent diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index aa61b12e7b8..3f421393a75 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -345,7 +345,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { case .Fetching: self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: resource, statsCategory: statsCategory ?? .generic).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: resource, statsCategory: statsCategory ?? .generic).start()) default: break } diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index d48cbf06d33..ca02fd657df 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -385,7 +385,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD case .Fetching: context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) default: break } diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index aab16e3fa9e..54cb1eeeed1 100644 --- a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift @@ -323,7 +323,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { case .Fetching: context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) default: break } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 1d5f95c3de1..d177481c665 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -52,10 +52,12 @@ enum ChatMediaGalleryThumbnail: Equatable { final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { private let account: Account + private let userLocation: MediaResourceUserLocation private let thumbnail: ChatMediaGalleryThumbnail - init?(account: Account, mediaReference: AnyMediaReference) { + init?(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) { self.account = account + self.userLocation = userLocation if let imageReference = mediaReference.concrete(TelegramMediaImage.self) { self.thumbnail = .image(imageReference) } else if let fileReference = mediaReference.concrete(TelegramMediaFile.self) { @@ -81,19 +83,19 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem { switch self.thumbnail { case let .image(imageReference): if let representation = largestImageRepresentation(imageReference.media.representations) { - return (mediaGridMessagePhoto(account: self.account, photoReference: imageReference), representation.dimensions.cgSize) + return (mediaGridMessagePhoto(account: self.account, userLocation: self.userLocation, photoReference: imageReference), representation.dimensions.cgSize) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } case let .video(fileReference): if let representation = largestImageRepresentation(fileReference.media.previewRepresentations) { - return (mediaGridMessageVideo(postbox: self.account.postbox, videoReference: fileReference), representation.dimensions.cgSize) + return (mediaGridMessageVideo(postbox: self.account.postbox, userLocation: self.userLocation, videoReference: fileReference), representation.dimensions.cgSize) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } case let .file(fileReference): if let representation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - return (chatWebpageSnippetFile(account: self.account, mediaReference: fileReference.abstract, representation: representation), representation.dimensions.cgSize) + return (chatWebpageSnippetFile(account: self.account, userLocation: self.userLocation, mediaReference: fileReference.abstract, representation: representation), representation.dimensions.cgSize) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } @@ -132,19 +134,19 @@ class ChatImageGalleryItem: GalleryItem { node.setMessage(self.message, displayInfo: !self.displayInfoOnTop) for media in self.message.media { if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage { - node.setImage(imageReference: .message(message: MessageReference(self.message), media: image)) + node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) } else if let image = media as? TelegramMediaImage { - node.setImage(imageReference: .message(message: MessageReference(self.message), media: image)) + node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) break } else if let file = media as? TelegramMediaFile, file.mimeType.hasPrefix("image/") { - node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file)) + node.setFile(context: self.context, userLocation: .peer(self.message.id.peerId), fileReference: .message(message: MessageReference(self.message), media: file)) break } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { if let image = content.image { - node.setImage(imageReference: .message(message: MessageReference(self.message), media: image)) + node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image)) break } else if let file = content.file, file.mimeType.hasPrefix("image/") { - node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file)) + node.setFile(context: self.context, userLocation: .peer(self.message.id.peerId), fileReference: .message(message: MessageReference(self.message), media: file)) break } } @@ -183,7 +185,7 @@ class ChatImageGalleryItem: GalleryItem { } } if let mediaReference = mediaReference { - if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: mediaReference) { + if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(self.message.id.peerId), mediaReference: mediaReference) { return (Int64(id), item) } } @@ -323,12 +325,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setMessage(message, displayInfo: displayInfo) } - fileprivate func setImage(imageReference: ImageMediaReference) { + fileprivate func setImage(userLocation: MediaResourceUserLocation, imageReference: ImageMediaReference) { if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: imageReference.media) { if let largestSize = largestRepresentationForPhoto(imageReference.media) { let displaySize = largestSize.dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: self.context.account.postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: false), synchronousLoad: false) + let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: self.context.account.postbox, userLocation: userLocation, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: false), synchronousLoad: false) |> map { [weak self] _, quality, generate -> (TransformImageArguments) -> DrawingContext? in Queue.mainQueue().async { guard let strongSelf = self else { @@ -418,7 +420,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.zoomableContent = (largestSize.dimensions.cgSize, self.imageNode) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: imageReference.resourceReference(largestSize.resource)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start()) self.setupStatus(resource: largestSize.resource) } else { self._ready.set(.single(Void())) @@ -643,7 +645,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { }) } - func setFile(context: AccountContext, fileReference: FileMediaReference) { + func setFile(context: AccountContext, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference) { if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) { if var largestSize = fileReference.media.dimensions { var displaySize = largestSize.cgSize.dividedByScreenScale() @@ -669,7 +671,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.updateImageFromFile(path: data.path) })) } else {*/ - self.imageNode.setSignal(chatMessageImageFile(account: context.account, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) //} self.zoomableContent = (largestSize.cgSize, self.imageNode) @@ -932,7 +934,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { case .Fetching: self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: resource, statsCategory: statsCategory ?? .generic).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .image, reference: resource, statsCategory: statsCategory ?? .generic).start()) default: break } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index b061a38700e..8d646bd36b2 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -126,13 +126,13 @@ public class UniversalVideoGalleryItem: GalleryItem { } } if let mediaReference = mediaReference { - if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: mediaReference) { + if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) { return (Int64(id), item) } } } } else if case let .webPage(webPage, media, _) = contentInfo, let file = media as? TelegramMediaFile { - if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, mediaReference: .webPage(webPage: WebpageReference(webPage), media: file)) { + if let item = ChatMediaGalleryThumbnailItem(account: self.context.account, userLocation: .other, mediaReference: .webPage(webPage: WebpageReference(webPage), media: file)) { return (0, item) } } @@ -1012,43 +1012,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } func setupItem(_ item: UniversalVideoGalleryItem) { - if self.item?.content.id != item.content.id { - func parseChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] { - var existingTimecodes = Set() - var timecodeRanges: [(NSRange, TelegramTimecode)] = [] - var lineRanges: [NSRange] = [] - string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in - if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode { - if !existingTimecodes.contains(timecode.time) { - timecodeRanges.append((range, timecode)) - existingTimecodes.insert(timecode.time) - } - } - }) - (string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in - lineRanges.append(range) - }) - - var chapters: [MediaPlayerScrubbingChapter] = [] - for (timecodeRange, timecode) in timecodeRanges { - inner: for lineRange in lineRanges { - if lineRange.contains(timecodeRange.location) { - if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 { - var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "") - title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters) - chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time)) - } - break inner - } - } - } - - return chapters - } - - var chapters = parseChapters(item.caption) + if self.item?.content.id != item.content.id { + var chapters = parseMediaPlayerChapters(item.caption) if chapters.isEmpty, let description = item.description { - chapters = parseChapters(description) + chapters = parseMediaPlayerChapters(description) } let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters) self.scrubberView = scrubberView @@ -1102,7 +1069,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isEnhancedWebPlayer = false if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated - self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, fileReference: content.fileReference) + self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, userLocation: content.userLocation, userContentType: .video, fileReference: content.fileReference) } else if let _ = item.content as? SystemVideoContent { self._title.set(.single(item.presentationData.strings.Message_Video)) } else if let content = item.content as? WebEmbedVideoContent { @@ -2565,7 +2532,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let strongSelf = self { switch strongSelf.fetchStatus { case .Local: - let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .message(message: MessageReference(message), media: file)) + let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return @@ -2710,7 +2677,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } let baseNavigationController = strongSelf.baseNavigationController() baseNavigationController?.view.endEditing(true) - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: Array(packs.prefix(1)), sendSticker: nil, actionPerformed: { actions in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in if let (info, items, action) = actions.first { let animateInAsReplacement = false switch action { diff --git a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift index 34845f3f131..d397279f8a5 100644 --- a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift +++ b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift @@ -12,13 +12,14 @@ import GalleryUI private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem { let account: Account + let userLocation: MediaResourceUserLocation let mediaReference: AnyMediaReference func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { if let imageReferene = mediaReference.concrete(TelegramMediaImage.self), let representation = largestImageRepresentation(imageReferene.media.representations) { - return (mediaGridMessagePhoto(account: self.account, photoReference: imageReferene), representation.dimensions.cgSize) + return (mediaGridMessagePhoto(account: self.account, userLocation: self.userLocation, photoReference: imageReferene), representation.dimensions.cgSize) } else if let fileReference = mediaReference.concrete(TelegramMediaFile.self), let dimensions = fileReference.media.dimensions { - return (mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference), dimensions.cgSize) + return (mediaGridMessageVideo(postbox: account.postbox, userLocation: self.userLocation, videoReference: fileReference), dimensions.cgSize) } else { return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0)) } @@ -42,6 +43,7 @@ class InstantImageGalleryItem: GalleryItem { let context: AccountContext let presentationData: PresentationData + let userLocation: MediaResourceUserLocation let imageReference: ImageMediaReference let caption: NSAttributedString let credit: NSAttributedString @@ -49,8 +51,9 @@ class InstantImageGalleryItem: GalleryItem { let openUrl: (InstantPageUrlItem) -> Void let openUrlOptions: (InstantPageUrlItem) -> Void - init(context: AccountContext, presentationData: PresentationData, itemId: AnyHashable, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, presentationData: PresentationData, itemId: AnyHashable, userLocation: MediaResourceUserLocation, imageReference: ImageMediaReference, caption: NSAttributedString, credit: NSAttributedString, location: InstantPageGalleryEntryLocation?, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) { self.itemId = itemId + self.userLocation = userLocation self.context = context self.presentationData = presentationData self.imageReference = imageReference @@ -64,7 +67,7 @@ class InstantImageGalleryItem: GalleryItem { func node(synchronous: Bool) -> GalleryItemNode { let node = InstantImageGalleryItemNode(context: self.context, presentationData: self.presentationData, openUrl: self.openUrl, openUrlOptions: self.openUrlOptions) - node.setImage(imageReference: self.imageReference) + node.setImage(userLocation: self.userLocation, imageReference: self.imageReference) if let location = self.location { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.position + 1)", "\(location.totalCount)").string)) @@ -86,7 +89,7 @@ class InstantImageGalleryItem: GalleryItem { } func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return (0, InstantImageGalleryThumbnailItem(account: self.context.account, mediaReference: imageReference.abstract)) + return (0, InstantImageGalleryThumbnailItem(account: self.context.account, userLocation: self.userLocation, mediaReference: imageReference.abstract)) } } @@ -98,6 +101,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate let _title = Promise() private let footerContentNode: InstantPageGalleryFooterContentNode + private var userLocation: MediaResourceUserLocation? private var contextAndMedia: (AccountContext, AnyMediaReference)? private var fetchDisposable = MetaDisposable() @@ -136,14 +140,16 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setCaption(caption, credit: credit) } - fileprivate func setImage(imageReference: ImageMediaReference) { + fileprivate func setImage(userLocation: MediaResourceUserLocation, imageReference: ImageMediaReference) { + self.userLocation = userLocation + if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: imageReference.media) { if let largestSize = largestRepresentationForPhoto(imageReference.media) { let displaySize = largestSize.dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), emptyColor: .black))() - self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, photoReference: imageReference), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, userLocation: userLocation, photoReference: imageReference), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions.cgSize, self.imageNode) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: imageReference.resourceReference(largestSize.resource)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start()) } else { self._ready.set(.single(Void())) } @@ -152,12 +158,14 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.setShareMedia(imageReference.abstract) } - func setFile(context: AccountContext, fileReference: FileMediaReference) { + func setFile(context: AccountContext, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference) { + self.userLocation = userLocation + if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) { if let largestSize = fileReference.media.dimensions { let displaySize = largestSize.cgSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(chatMessageImageFile(account: context.account, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) + self.imageNode.setSignal(chatMessageImageFile(account: context.account, userLocation: userLocation, fileReference: fileReference, thumbnail: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.cgSize, self.imageNode) } else { self._ready.set(.single(Void())) @@ -296,7 +304,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { if let (context, media) = self.contextAndMedia, let fileReference = media.concrete(TelegramMediaFile.self) { if isVisible { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) } else { self.fetchDisposable.set(nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift index 5356646d0d5..5716720974d 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAnchorItem.swift @@ -28,7 +28,7 @@ final class InstantPageAnchorItem: InstantPageItem { func drawInTile(context: CGContext) { } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift index f83b6ab86f3..fb1b3f3f779 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleItem.swift @@ -13,6 +13,7 @@ final class InstantPageArticleItem: InstantPageItem { let wantsNode: Bool = true let separatesTiles: Bool = false let medias: [InstantPageMedia] = [] + let userLocation: MediaResourceUserLocation let webPage: TelegramMediaWebpage let contentItems: [InstantPageItem] @@ -23,8 +24,9 @@ final class InstantPageArticleItem: InstantPageItem { let rtl: Bool let hasRTL: Bool - init(frame: CGRect, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, hasRTL: Bool) { + init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, hasRTL: Bool) { self.frame = frame + self.userLocation = userLocation self.webPage = webPage self.contentItems = contentItems self.contentSize = contentSize @@ -35,7 +37,7 @@ final class InstantPageArticleItem: InstantPageItem { self.hasRTL = hasRTL } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageArticleNode(context: context, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, openUrl: openUrl) } @@ -71,7 +73,7 @@ final class InstantPageArticleItem: InstantPageItem { } } -func layoutArticleItem(theme: InstantPageTheme, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem { +func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem { let inset: CGFloat = 17.0 let imageSpacing: CGFloat = 10.0 var sideInset = inset @@ -116,5 +118,5 @@ func layoutArticleItem(theme: InstantPageTheme, webPage: TelegramMediaWebpage, t } let contentSize = CGSize(width: boundingWidth, height: contentHeight) - return InstantPageArticleItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), webPage: webPage, contentItems: contentItems, contentSize: contentSize, cover: cover, url: url, webpageId: webpageId, rtl: rtl || hasRTL, hasRTL: hasRTL) + return InstantPageArticleItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), userLocation: userLocation, webPage: webPage, contentItems: contentItems, contentSize: contentSize, cover: cover, url: url, webpageId: webpageId, rtl: rtl || hasRTL, hasRTL: hasRTL) } diff --git a/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift b/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift index 7bde8c039cf..a13a3099d5e 100644 --- a/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageArticleNode.swift @@ -55,8 +55,8 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode { imageNode.isUserInteractionEnabled = false let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) - imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: item.userLocation, photoReference: imageReference)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: item.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) self.imageNode = imageNode self.addSubnode(imageNode) diff --git a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift index 21fded36a45..8a2ed3c11cc 100644 --- a/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageAudioItem.swift @@ -24,7 +24,7 @@ final class InstantPageAudioItem: InstantPageItem { self.medias = [media] } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia) } diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index 5d342c96a78..999ba1d5bb2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -13,7 +13,7 @@ final class InstantPageContentNode : ASDisplayNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private let openMedia: (InstantPageMedia) -> Void @@ -40,11 +40,11 @@ final class InstantPageContentNode : ASDisplayNode { private var previousVisibleBounds: CGRect? - init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.theme = theme self.openMedia = openMedia @@ -188,7 +188,7 @@ final class InstantPageContentNode : ASDisplayNode { if itemNode == nil { let itemIndex = itemIndex let detailsIndex = detailsIndex - if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) diff --git a/submodules/InstantPageUI/Sources/InstantPageController.swift b/submodules/InstantPageUI/Sources/InstantPageController.swift index 05cabd89dda..25cb5dcf64e 100644 --- a/submodules/InstantPageUI/Sources/InstantPageController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageController.swift @@ -8,6 +8,16 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext +public struct InstantPageSourceLocation { + public var userLocation: MediaResourceUserLocation + public var peerType: MediaAutoDownloadPeerType + + public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) { + self.userLocation = userLocation + self.peerType = peerType + } +} + public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, String?)? { for media in message.media { if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { @@ -61,7 +71,7 @@ public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, Str public final class InstantPageController: ViewController { private let context: AccountContext private var webPage: TelegramMediaWebpage - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let anchor: String? private var presentationData: PresentationData @@ -82,13 +92,13 @@ public final class InstantPageController: ViewController { private var settingsDisposable: Disposable? private var themeSettings: PresentationThemeSettings? - public init(context: AccountContext, webPage: TelegramMediaWebpage, sourcePeerType: MediaAutoDownloadPeerType, anchor: String? = nil) { + public init(context: AccountContext, webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation, anchor: String? = nil) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.webPage = webPage self.anchor = anchor - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation super.init(navigationBarPresentationData: nil) @@ -145,7 +155,7 @@ public final class InstantPageController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageControllerNode(controller: self, context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourcePeerType: self.sourcePeerType, getNavigationController: { [weak self] in + self.displayNode = InstantPageControllerNode(controller: self, context: self.context, settings: self.settings, themeSettings: self.themeSettings, presentationTheme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, autoNightModeTriggered: self.presentationData.autoNightModeTriggered, statusBar: self.statusBar, sourceLocation: self.sourceLocation, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a, blockInteraction: true) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 63beac8e35d..d66c110f821 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -29,7 +29,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { private let autoNightModeTriggered: Bool private var dateTimeFormat: PresentationDateTimeFormat private var theme: InstantPageTheme? - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private var manualThemeOverride: InstantPageThemeType? private let getNavigationController: () -> NavigationController? private let present: (ViewController, Any?) -> Void @@ -92,7 +92,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details) } - init(controller: InstantPageController, context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (EnginePeer) -> Void, navigateBack: @escaping () -> Void) { + init(controller: InstantPageController, context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourceLocation: InstantPageSourceLocation, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (EnginePeer) -> Void, navigateBack: @escaping () -> Void) { self.controller = controller self.context = context self.presentationTheme = presentationTheme @@ -106,7 +106,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { self.theme = settings.flatMap { settings in return instantPageThemeForType(instantPageThemeTypeForSettingsAndTime(themeSettings: themeSettings, settings: settings, time: themeReferenceDate, forceDarkTheme: autoNightModeTriggered).0, settings: settings) } - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.statusBar = statusBar self.getNavigationController = getNavigationController self.present = present @@ -445,7 +445,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) + let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) for (_, tileNode) in self.visibleTiles { tileNode.removeFromSupernode() @@ -593,7 +593,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let itemIndex = itemIndex let embedIndex = embedIndex let detailsIndex = detailsIndex - if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) @@ -1000,12 +1000,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() + let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() + let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage { @@ -1211,7 +1211,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { return } - let controller = InstantPageReferenceController(context: self.context, sourcePeerType: self.sourcePeerType, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in + let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in self?.openUrl(url) }, openUrlIn: { [weak self] url in self?.openUrlIn(url) @@ -1310,7 +1310,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { case let .result(webpage): if let webpage = webpage, case .Loaded = webpage.content { strongSelf.loadProgress.set(1.0) - strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: strongSelf.sourcePeerType, anchor: anchor)) + strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: strongSelf.sourceLocation, anchor: anchor)) } break case let .progress(progress): @@ -1457,7 +1457,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let centralIndex = centralIndex { - let controller = InstantPageGalleryController(context: self.context, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in + let controller = InstantPageGalleryController(context: self.context, userLocation: self.sourceLocation.userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in }, baseNavigationController: self.getNavigationController()) self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in if let strongSelf = self { diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift index 9d610105b79..f567bbe3261 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsItem.swift @@ -40,12 +40,12 @@ final class InstantPageDetailsItem: InstantPageItem { self.index = index } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { var expanded: Bool? if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] { expanded = currentlyExpanded } - return InstantPageDetailsNode(context: context, sourcePeerType: sourcePeerType, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded) + return InstantPageDetailsNode(context: context, sourceLocation: sourceLocation, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift index a65e0dc7b48..2cf212786d1 100644 --- a/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageDetailsNode.swift @@ -35,7 +35,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { var requestLayoutUpdate: ((Bool) -> Void)? - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder @@ -65,7 +65,7 @@ final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode { self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded) self.separatorNode = ASDisplayNode() - self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourcePeerType: sourcePeerType, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl) + self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourceLocation: sourceLocation, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, openPeer: openPeer, openUrl: openUrl) super.init() diff --git a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift index 122c8e07020..7bdb399ccf9 100644 --- a/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageFeedbackItem.swift @@ -21,7 +21,7 @@ final class InstantPageFeedbackItem: InstantPageItem { self.webPage = webPage } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageFeedbackNode(context: context, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl) } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 9ef20830020..716aef66b4f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -48,7 +48,7 @@ public struct InstantPageGalleryEntry: Equatable { return lhs.index == rhs.index && lhs.pageId == rhs.pageId && lhs.media == rhs.media && lhs.caption == rhs.caption && lhs.credit == rhs.credit && lhs.location == rhs.location } - func item(context: AccountContext, webPage: TelegramMediaWebpage, message: Message?, presentationData: PresentationData, fromPlayingVideo: Bool, landscape: Bool, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) -> GalleryItem { + func item(context: AccountContext, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, message: Message?, presentationData: PresentationData, fromPlayingVideo: Bool, landscape: Bool, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlOptions: @escaping (InstantPageUrlItem) -> Void) -> GalleryItem { let caption: NSAttributedString let credit: NSAttributedString @@ -97,7 +97,7 @@ public struct InstantPageGalleryEntry: Equatable { } if let image = self.media.media as? TelegramMediaImage { - return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } else if let file = self.media.media as? TelegramMediaFile { if file.isVideo { var indexData: GalleryItemIndexData? @@ -112,21 +112,21 @@ public struct InstantPageGalleryEntry: Equatable { nativeId = .instantPage(self.pageId, file.fileId) } - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackRate: { nil }, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackRate: { nil }, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) } else { var representations: [TelegramMediaImageRepresentation] = [] representations.append(contentsOf: file.previewRepresentations) if let dimensions = file.dimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) + return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) } } else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content { if webpageContent.url.hasSuffix(".m3u8") { - let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), content: .url(webpageContent.url), streamVideo: true, loopVideo: false) + let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false) return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in - let gallery = InstantPageGalleryController(context: context, webPage: webPage, entries: [self], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in + let gallery = InstantPageGalleryController(context: context, userLocation: userLocation, webPage: webPage, entries: [self], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in if let navigationController = navigationController { navigationController.replaceTopController(controller, animated: false, ready: ready) } @@ -136,7 +136,7 @@ public struct InstantPageGalleryEntry: Equatable { })) }), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackRate: { nil }, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) } else { - if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in + if let content = WebEmbedVideoContent(userLocation: userLocation, webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in }) { return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, playbackRate: { nil }, performAction: { _ in }, openActionOptions: { _, _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) @@ -164,6 +164,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable } private let context: AccountContext + private let userLocation: MediaResourceUserLocation private let webPage: TelegramMediaWebpage private let message: Message? private var presentationData: PresentationData @@ -201,8 +202,9 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable private var innerOpenUrl: (InstantPageUrlItem) -> Void private var openUrlOptions: (InstantPageUrlItem) -> Void - public init(context: AccountContext, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?) { + public init(context: AccountContext, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, message: Message? = nil, entries: [InstantPageGalleryEntry], centralIndex: Int, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?) { self.context = context + self.userLocation = userLocation self.webPage = webPage self.message = message self.fromPlayingVideo = fromPlayingVideo @@ -236,7 +238,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable strongSelf.centralEntryIndex = centralIndex if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(context: context, webPage: webPage, message: message, presentationData: strongSelf.presentationData, fromPlayingVideo: fromPlayingVideo, landscape: landscape, openUrl: strongSelf.innerOpenUrl, openUrlOptions: strongSelf.openUrlOptions) + $0.item(context: context, userLocation: userLocation, webPage: webPage, message: message, presentationData: strongSelf.presentationData, fromPlayingVideo: fromPlayingVideo, landscape: landscape, openUrl: strongSelf.innerOpenUrl, openUrlOptions: strongSelf.openUrlOptions) }), centralItemIndex: centralIndex) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in @@ -398,7 +400,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable } self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(context: self.context, webPage: self.webPage, message: self.message, presentationData: self.presentationData, fromPlayingVideo: self.fromPlayingVideo, landscape: self.landscape, openUrl: self.innerOpenUrl, openUrlOptions: self.openUrlOptions) + $0.item(context: self.context, userLocation: self.userLocation, webPage: self.webPage, message: self.message, presentationData: self.presentationData, fromPlayingVideo: self.fromPlayingVideo, landscape: self.landscape, openUrl: self.innerOpenUrl, openUrlOptions: self.openUrlOptions) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index 05aae91fd36..a08814737cd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -45,8 +45,8 @@ final class InstantPageImageItem: InstantPageItem { self.fit = fit } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { - return InstantPageImageNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + return InstantPageImageNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index f7ceffea21c..0aeea7c5015 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -49,7 +49,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var themeUpdated: Bool = false - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) { self.context = context self.theme = theme self.webPage = webPage @@ -74,15 +74,15 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) - self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) + self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) - if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) { - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) { + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) } self.fetchControls = FetchControls(fetch: { [weak self] manual in if let strongSelf = self { - strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) } }, cancel: { chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference) @@ -108,12 +108,12 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } else if let file = media.media as? TelegramMediaFile { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) if file.mimeType.hasPrefix("image/") { - if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourcePeerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) { - _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: fileReference).start() + if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) { + _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference).start() } - self.imageNode.setSignal(instantPageImageFile(account: context.account, fileReference: fileReference, fetched: true)) + self.imageNode.setSignal(instantPageImageFile(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference, fetched: true)) } else { - self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, videoReference: fileReference)) + self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, videoReference: fileReference)) } if file.isVideo { self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) @@ -133,8 +133,8 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource)) } else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) - self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference)) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerType: nil).start()) self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } diff --git a/submodules/InstantPageUI/Sources/InstantPageItem.swift b/submodules/InstantPageUI/Sources/InstantPageItem.swift index 0ef9bc22261..6804e8bc44d 100644 --- a/submodules/InstantPageUI/Sources/InstantPageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageItem.swift @@ -16,7 +16,7 @@ protocol InstantPageItem { func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? func matchesNode(_ node: InstantPageNode) -> Bool func linkSelectionRects(at point: CGPoint) -> [CGRect] diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index 4f157f5e4f2..3647452673b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -48,7 +48,7 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP } } -func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout { +func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout { let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in var items: [InstantPageItem] = [] @@ -101,7 +101,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins switch block { case let .cover(block): - return layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) case let .title(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .header, link: false) @@ -275,7 +275,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins var previousBlock: InstantPageBlock? var originY: CGFloat = contentSize.height for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0 let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing)) @@ -477,7 +477,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins var i = 0 for subItem in innerItems { let frame = mosaicLayout[i].0 - let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: true) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: true) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin)) i += 1 } @@ -545,7 +545,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) @@ -728,7 +728,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) @@ -791,7 +791,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins } let description = attributedStringForRichText(.plain(subtext ?? ""), styleStack: styleStack) - let item = layoutArticleItem(theme: theme, webPage: webpage, title: title, description: description, cover: cover, url: article.url, webpageId: article.webpageId, boundingWidth: boundingWidth, rtl: rtl) + let item = layoutArticleItem(theme: theme, userLocation: userLocation, webPage: webpage, title: title, description: description, cover: cover, url: article.url, webpageId: article.webpageId, boundingWidth: boundingWidth, rtl: rtl) item.frame = item.frame.offsetBy(dx: 0.0, dy: contentSize.height) contentSize.height += item.frame.height items.append(item) @@ -835,7 +835,7 @@ func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: Ins } } -func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { +func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -864,7 +864,7 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(webpage: webPage, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift index 3c2a91b33d1..0bc839724ff 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceItem.swift @@ -27,7 +27,7 @@ final class InstantPagePeerReferenceItem: InstantPageItem { self.rtl = rtl } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPagePeerReferenceNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer) } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index a68ae01c398..79a32538c3a 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -29,8 +29,8 @@ final class InstantPagePlayableVideoItem: InstantPageItem { self.interactive = interactive } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { - return InstantPagePlayableVideoNode(context: context, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + return InstantPagePlayableVideoNode(context: context, userLocation: sourceLocation.userLocation, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index 86b0921dce0..dc63946e032 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -19,6 +19,7 @@ private struct FetchControls { final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, GalleryItemTransitionNode { private let context: AccountContext let media: InstantPageMedia + let userLocation: MediaResourceUserLocation private let interactive: Bool private let openMedia: (InstantPageMedia) -> Void private var fetchControls: FetchControls? @@ -38,8 +39,9 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler return nil } - init(context: AccountContext, webPage: TelegramMediaWebpage, theme: InstantPageTheme, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { + init(context: AccountContext, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, theme: InstantPageTheme, media: InstantPageMedia, interactive: Bool, openMedia: @escaping (InstantPageMedia) -> Void) { self.context = context + self.userLocation = userLocation self.media = media self.interactive = interactive self.openMedia = openMedia @@ -55,7 +57,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler streamVideo = isMediaStreamable(media: file) } - self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor), priority: .embedded, autoplay: true) + self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor), priority: .embedded, autoplay: true) self.videoNode.isUserInteractionEnabled = false self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) @@ -65,7 +67,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler self.addSubnode(self.videoNode) if let file = media.media as? TelegramMediaFile { - self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start()) + self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start()) self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in displayLinkDispatcher.dispatch { diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift index 825827066b3..64860688c03 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceController.swift @@ -16,7 +16,7 @@ final class InstantPageReferenceController: ViewController { private var animatedIn = false private let context: AccountContext - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private let webPage: TelegramMediaWebpage private let anchorText: NSAttributedString @@ -24,9 +24,9 @@ final class InstantPageReferenceController: ViewController { private let openUrlIn: (InstantPageUrlItem) -> Void private let present: (ViewController, Any?) -> Void - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.theme = theme self.webPage = webPage self.anchorText = anchorText @@ -44,7 +44,7 @@ final class InstantPageReferenceController: ViewController { } override public func loadDisplayNode() { - self.displayNode = InstantPageReferenceControllerNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) + self.displayNode = InstantPageReferenceControllerNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, anchorText: self.anchorText, openUrl: self.openUrl, openUrlIn: self.openUrlIn, present: self.present) self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index 548b6abe1a9..3a13d373cbf 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -13,7 +13,7 @@ import TelegramUIPreferences class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private var presentationData: PresentationData private let webPage: TelegramMediaWebpage @@ -39,9 +39,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie var dismiss: (() -> Void)? var close: (() -> Void)? - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = theme self.webPage = webPage @@ -204,7 +204,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie let sideInset: CGFloat = 16.0 let (_, items, contentSize) = layoutTextItemWithString(self.anchorText, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage) - let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourcePeerType: self.sourcePeerType, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in }) + let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourceLocation: self.sourceLocation, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, openPeer: { _ in }, openUrl: { _ in }) transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: width, height: contentSize.height))) self.contentContainerNode.insertSubnode(contentNode, at: 0) self.contentNode = contentNode diff --git a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift index a1703ed3ff3..b5b354686ff 100644 --- a/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageShapeItem.swift @@ -62,7 +62,7 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift index 2d19757b57d..d9e081d15cc 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift @@ -21,8 +21,8 @@ final class InstantPageSlideshowItem: InstantPageItem { self.medias = medias } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { - return InstantPageSlideshowNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + return InstantPageSlideshowNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia) } func matchesAnchor(_ anchor: String) -> Bool { diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index a45ee2f1eb7..3449b277276 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -64,7 +64,7 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private let webPage: TelegramMediaWebpage private let openMedia: (InstantPageMedia) -> Void @@ -98,9 +98,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe } } - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, pageGap: CGFloat = 0.0) { self.context = context - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.theme = theme self.webPage = webPage self.openMedia = openMedia @@ -182,7 +182,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe let media = self.items[index] let contentNode: ASDisplayNode if let _ = media.media as? TelegramMediaImage { - contentNode = InstantPageImageNode(context: self.context, sourcePeerType: self.sourcePeerType, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: nil, pinchPreviewFinished: nil) + contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: nil, pinchPreviewFinished: nil) } else if let _ = media.media as? TelegramMediaFile { contentNode = ASDisplayNode() } else { @@ -381,10 +381,10 @@ final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { private let pagerNode: InstantPageSlideshowPagerNode private let pageControlNode: PageControlNode - init(context: AccountContext, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { + init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void) { self.medias = medias - self.pagerNode = InstantPageSlideshowPagerNode(context: context, sourcePeerType: sourcePeerType, theme: theme, webPage: webPage, openMedia: openMedia, longPressMedia: longPressMedia) + self.pagerNode = InstantPageSlideshowPagerNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: webPage, openMedia: openMedia, longPressMedia: longPressMedia) self.pagerNode.replaceItems(medias, centralItemIndex: nil) self.pageControlNode = PageControlNode(dotColor: .white, inactiveDotColor: UIColor(white: 1.0, alpha: 0.5)) diff --git a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift index 89b4ab0e93e..d8146a8b30a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift @@ -13,7 +13,7 @@ final class InstantPageSubContentNode : ASDisplayNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder - private let sourcePeerType: MediaAutoDownloadPeerType + private let sourceLocation: InstantPageSourceLocation private let theme: InstantPageTheme private let openMedia: (InstantPageMedia) -> Void @@ -40,11 +40,11 @@ final class InstantPageSubContentNode : ASDisplayNode { private var previousVisibleBounds: CGRect? - init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { + init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) { self.context = context self.strings = strings self.nameDisplayOrder = nameDisplayOrder - self.sourcePeerType = sourcePeerType + self.sourceLocation = sourceLocation self.theme = theme self.openMedia = openMedia @@ -188,7 +188,7 @@ final class InstantPageSubContentNode : ASDisplayNode { if itemNode == nil { let itemIndex = itemIndex let detailsIndex = detailsIndex - if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in + if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in self?.openMedia(media) }, longPressMedia: { [weak self] media in self?.longPressMedia(media) diff --git a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift index 4188955b66a..fe99459868d 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTableItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTableItem.swift @@ -200,12 +200,12 @@ final class InstantPageTableItem: InstantPageScrollableItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { var additionalNodes: [InstantPageNode] = [] for cell in self.cells { for item in cell.additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index d1c9287ecdf..e707cc6b530 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -436,7 +436,7 @@ final class InstantPageTextItem: InstantPageItem { return false } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return nil } @@ -485,11 +485,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem { context.restoreGState() } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { var additionalNodes: [InstantPageNode] = [] for item in additionalItems { if item.wantsNode { - if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourcePeerType: sourcePeerType, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { + if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) { node.frame = item.frame additionalNodes.append(node) } diff --git a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift index 71d24dc6d86..4292366e7a2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageWebEmbedItem.swift @@ -25,7 +25,7 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourcePeerType: MediaAutoDownloadPeerType, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? { return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight) } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index aff78e071a8..661a90ca71a 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -15,7 +15,7 @@ func createEmitterBehavior(type: String) -> NSObject { return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) } -private func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? { +func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? { var size = originalSize var position = position var scale: CGFloat = 1.0 @@ -58,8 +58,7 @@ public class InvisibleInkDustNode: ASDisplayNode { private let emitterMaskFillNode: ASDisplayNode public var isRevealed = false - - private var exploding = false + private var isExploding = false public init(textNode: TextNode?) { self.textNode = textNode @@ -158,8 +157,8 @@ public class InvisibleInkDustNode: ASDisplayNode { transition.updateAlpha(node: self, alpha: 1.0) transition.updateAlpha(node: textNode, alpha: 0.0) - if self.exploding { - self.exploding = false + if self.isExploding { + self.isExploding = false self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") } } @@ -171,7 +170,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } self.isRevealed = true - self.exploding = true + self.isExploding = true let position = gestureRecognizer.location(in: self.view) self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") @@ -227,66 +226,13 @@ public class InvisibleInkDustNode: ASDisplayNode { } Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { - self.exploding = false + self.isExploding = false self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") self.textSpotNode.layer.removeAllAnimations() self.emitterSpotNode.layer.removeAllAnimations() self.emitterMaskFillNode.layer.removeAllAnimations() } - - var spoilersLength: Int = 0 - if let spoilers = textNode.cachedLayout?.spoilers { - for spoiler in spoilers { - spoilersLength += spoiler.0.length - } - } - - let timeToRead = min(45.0, ceil(max(4.0, Double(spoilersLength) * 0.04))) - Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) { - if let (_, color, _, _, _) = self.currentParams { - let colorSpace = CGColorSpaceCreateDeviceRGB() - let animation = POPBasicAnimation() - animation.property = (POPAnimatableProperty.property(withName: "color", initializer: { property in - property?.readBlock = { node, values in - if let color = (node as! InvisibleInkDustNode).emitter?.color { - if let a = color.components { - values?[0] = a[0] - values?[1] = a[1] - values?[2] = a[2] - values?[3] = a[3] - } - } - } - property?.writeBlock = { node, values in - if let values = values, let color = CGColor(colorSpace: colorSpace, components: values) { - (node as! InvisibleInkDustNode).animColor = color - (node as! InvisibleInkDustNode).updateEmitter() - } - } - property?.threshold = 0.4 - }) as! POPAnimatableProperty) - animation.fromValue = self.emitter?.color - animation.toValue = color - animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) - animation.duration = 0.1 - animation.completionBlock = { [weak self] _, _ in - if let strongSelf = self { - strongSelf.animColor = nil - strongSelf.updateEmitter() - } - } - self.pop_add(animation, forKey: "color") - } - - Queue.mainQueue().after(0.15) { - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) - transition.updateAlpha(node: self, alpha: 1.0) - transition.updateAlpha(node: textNode, alpha: 0.0, completion: { [weak self] _ in - self?.isRevealed = false - }) - } - } } private func updateEmitter() { diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 4968785bae1..3de61b60818 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -6,6 +6,89 @@ import Display import AppBundle import LegacyComponents +public class MediaDustLayer: CALayer { + private var emitter: CAEmitterCell? + private var emitterLayer: CAEmitterLayer? + + private var size: CGSize? + + override public init() { + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupEmitterLayerIfNeeded() { + guard self.emitterLayer == nil else { + return + } + + let emitter = CAEmitterCell() + emitter.color = UIColor(rgb: 0xffffff, alpha: 0.0).cgColor + emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage + emitter.contentsScale = 1.8 + emitter.emissionRange = .pi * 2.0 + emitter.lifetime = 8.0 + emitter.scale = 0.5 + emitter.velocityRange = 0.0 + emitter.name = "dustCell" + emitter.alphaRange = 1.0 + emitter.setValue("point", forKey: "particleType") + emitter.setValue(1.0, forKey: "mass") + emitter.setValue(0.01, forKey: "massRange") + self.emitter = emitter + + let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + alphaBehavior.setValue("color.alpha", forKey: "keyPath") + alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") + alphaBehavior.setValue(true, forKey: "additive") + + let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + scaleBehavior.setValue("scale", forKey: "keyPath") + scaleBehavior.setValue([0.0, 0.5], forKey: "values") + scaleBehavior.setValue([0.0, 0.05], forKey: "locations") + + let behaviors = [alphaBehavior, scaleBehavior] + + let emitterLayer = CAEmitterLayer() + emitterLayer.masksToBounds = true + emitterLayer.allowsGroupOpacity = true + emitterLayer.lifetime = 1 + emitterLayer.emitterCells = [emitter] + emitterLayer.seed = arc4random() + emitterLayer.emitterShape = .rectangle + emitterLayer.setValue(behaviors, forKey: "emitterBehaviors") + self.addSublayer(emitterLayer) + + self.emitterLayer = emitterLayer + } + + private func updateEmitter() { + guard let size = self.size else { + return + } + + self.setupEmitterLayerIfNeeded() + + self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size) + self.emitterLayer?.emitterSize = size + self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + let square = Float(size.width * size.height) + Queue.mainQueue().async { + self.emitter?.birthRate = min(100000.0, square * 0.02) + } + } + + public func updateLayout(size: CGSize) { + self.size = size + + self.updateEmitter() + } +} + public class MediaDustNode: ASDisplayNode { private var currentParams: (size: CGSize, color: UIColor)? private var animColor: CGColor? @@ -13,15 +96,37 @@ public class MediaDustNode: ASDisplayNode { private var emitterNode: ASDisplayNode private var emitter: CAEmitterCell? private var emitterLayer: CAEmitterLayer? - + + private let emitterMaskNode: ASDisplayNode + private let emitterSpotNode: ASImageNode + private let emitterMaskFillNode: ASDisplayNode + + public var isRevealed = false + private var isExploding = false + + public var revealed: () -> Void = {} + public var tapped: () -> Void = {} + public override init() { self.emitterNode = ASDisplayNode() self.emitterNode.isUserInteractionEnabled = false self.emitterNode.clipsToBounds = true + + self.emitterMaskNode = ASDisplayNode() + self.emitterSpotNode = ASImageNode() + self.emitterSpotNode.contentMode = .scaleToFill + self.emitterSpotNode.isUserInteractionEnabled = false + + self.emitterMaskFillNode = ASDisplayNode() + self.emitterMaskFillNode.backgroundColor = .white + self.emitterMaskFillNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.emitterNode) + + self.emitterMaskNode.addSubnode(self.emitterSpotNode) + self.emitterMaskNode.addSubnode(self.emitterMaskFillNode) } public override func didLoad() { @@ -51,8 +156,25 @@ public class MediaDustNode: ASDisplayNode { scaleBehavior.setValue("scale", forKey: "keyPath") scaleBehavior.setValue([0.0, 0.5], forKey: "values") scaleBehavior.setValue([0.0, 0.05], forKey: "locations") - - let behaviors = [alphaBehavior, scaleBehavior] + + let randomAttractor0 = createEmitterBehavior(type: "simpleAttractor") + randomAttractor0.setValue("randomAttractor0", forKey: "name") + randomAttractor0.setValue(20, forKey: "falloff") + randomAttractor0.setValue(35, forKey: "radius") + randomAttractor0.setValue(5, forKey: "stiffness") + randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position") + + let randomAttractor1 = createEmitterBehavior(type: "simpleAttractor") + randomAttractor1.setValue("randomAttractor1", forKey: "name") + randomAttractor1.setValue(20, forKey: "falloff") + randomAttractor1.setValue(35, forKey: "radius") + randomAttractor1.setValue(5, forKey: "stiffness") + randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position") + + let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + fingerAttractor.setValue("fingerAttractor", forKey: "name") + + let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior] let emitterLayer = CAEmitterLayer() emitterLayer.masksToBounds = true @@ -62,14 +184,145 @@ public class MediaDustNode: ASDisplayNode { emitterLayer.seed = arc4random() emitterLayer.emitterShape = .rectangle emitterLayer.setValue(behaviors, forKey: "emitterBehaviors") - + + emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness") + emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + self.emitterLayer = emitterLayer self.emitterNode.layer.addSublayer(emitterLayer) self.updateEmitter() + + self.setupRandomAnimations() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))) } + + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { + guard !self.isRevealed else { + return + } + + self.tapped() + + self.isRevealed = true + self.isExploding = true + + let position = gestureRecognizer.location(in: self.view) + self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") + let maskSize = self.emitterNode.frame.size + Queue.concurrentDefaultQueue().async { + let emitterMaskImage = generateMaskImage(size: maskSize, position: position, inverse: true) + + Queue.mainQueue().async { + self.emitterSpotNode.image = emitterMaskImage + } + } + + Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { + let xFactor = (position.x / self.emitterNode.frame.width - 0.5) * 2.0 + let yFactor = (position.y / self.emitterNode.frame.height - 0.5) * 2.0 + let maxFactor = max(abs(xFactor), abs(yFactor)) + + let scaleAddition = maxFactor * 4.0 + let durationAddition = -maxFactor * 0.2 + + self.supernode?.view.mask = self.emitterMaskNode.view + self.emitterSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0) + + self.emitterSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height) + self.emitterSpotNode.position = position + self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.45 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in + self?.revealed() + self?.alpha = 0.0 + self?.supernode?.view.mask = nil + + }) + self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { + self.isExploding = false + self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + + self.emitterSpotNode.layer.removeAllAnimations() + self.emitterMaskFillNode.layer.removeAllAnimations() + } + } + + private var didSetupAnimations = false + private func setupRandomAnimations() { + guard self.frame.width > 0.0, self.emitterLayer != nil, !self.didSetupAnimations else { + return + } + self.didSetupAnimations = true + + let falloffAnimation1 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor0.falloff") + falloffAnimation1.beginTime = 0.0 + falloffAnimation1.fillMode = .both + falloffAnimation1.isRemovedOnCompletion = false + falloffAnimation1.autoreverses = true + falloffAnimation1.repeatCount = .infinity + falloffAnimation1.duration = 2.0 + falloffAnimation1.fromValue = -20.0 as NSNumber + falloffAnimation1.toValue = 60.0 as NSNumber + falloffAnimation1.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.emitterLayer?.add(falloffAnimation1, forKey: "emitterBehaviors.randomAttractor0.falloff") + + let positionAnimation1 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor0.position") + positionAnimation1.beginTime = 0.0 + positionAnimation1.fillMode = .both + positionAnimation1.isRemovedOnCompletion = false + positionAnimation1.autoreverses = true + positionAnimation1.repeatCount = .infinity + positionAnimation1.duration = 3.0 + positionAnimation1.calculationMode = .discrete + + let xInset1: CGFloat = self.frame.width * 0.2 + let yInset1: CGFloat = self.frame.height * 0.2 + var positionValues1: [CGPoint] = [] + for _ in 0 ..< 35 { + positionValues1.append(CGPoint(x: CGFloat.random(in: xInset1 ..< self.frame.width - xInset1), y: CGFloat.random(in: yInset1 ..< self.frame.height - yInset1))) + } + positionAnimation1.values = positionValues1 + + self.emitterLayer?.add(positionAnimation1, forKey: "emitterBehaviors.randomAttractor0.position") + + let falloffAnimation2 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor1.falloff") + falloffAnimation2.beginTime = 0.0 + falloffAnimation2.fillMode = .both + falloffAnimation2.isRemovedOnCompletion = false + falloffAnimation2.autoreverses = true + falloffAnimation2.repeatCount = .infinity + falloffAnimation2.duration = 2.0 + falloffAnimation2.fromValue = -20.0 as NSNumber + falloffAnimation2.toValue = 60.0 as NSNumber + falloffAnimation2.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.emitterLayer?.add(falloffAnimation2, forKey: "emitterBehaviors.randomAttractor1.falloff") + + let positionAnimation2 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor1.position") + positionAnimation2.beginTime = 0.0 + positionAnimation2.fillMode = .both + positionAnimation2.isRemovedOnCompletion = false + positionAnimation2.autoreverses = true + positionAnimation2.repeatCount = .infinity + positionAnimation2.duration = 3.0 + positionAnimation2.calculationMode = .discrete + + let xInset2: CGFloat = self.frame.width * 0.1 + let yInset2: CGFloat = self.frame.height * 0.1 + var positionValues2: [CGPoint] = [] + for _ in 0 ..< 35 { + positionValues2.append(CGPoint(x: CGFloat.random(in: xInset2 ..< self.frame.width - xInset2), y: CGFloat.random(in: yInset2 ..< self.frame.height - yInset2))) + } + positionAnimation2.values = positionValues2 + + self.emitterLayer?.add(positionAnimation2, forKey: "emitterBehaviors.randomAttractor1.position") + } + private func updateEmitter() { guard let (size, _) = self.currentParams else { return @@ -79,19 +332,36 @@ public class MediaDustNode: ASDisplayNode { self.emitterLayer?.emitterSize = size self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + let radius = max(size.width, size.height) + self.emitterLayer?.setValue(max(size.width, size.height), forKeyPath: "emitterBehaviors.fingerAttractor.radius") + self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff") + let square = Float(size.width * size.height) Queue.mainQueue().async { - self.emitter?.birthRate = min(100000.0, square * 0.016) + self.emitter?.birthRate = min(100000.0, square * 0.02) } } - - public func update(size: CGSize, color: UIColor) { + + public func update(size: CGSize, color: UIColor, transition: ContainedViewLayoutTransition) { self.currentParams = (size, color) - - self.emitterNode.frame = CGRect(origin: CGPoint(), size: size) + + let bounds = CGRect(origin: .zero, size: size) + transition.updateFrame(node: self.emitterNode, frame: bounds) + + self.emitterMaskNode.frame = bounds + self.emitterMaskFillNode.frame = bounds if self.isNodeLoaded { self.updateEmitter() + self.setupRandomAnimations() + } + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if !self.isRevealed { + return super.point(inside: point, with: event) + } else { + return false } } } diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index ae6e8244e22..b7ee3001600 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -198,7 +198,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio } else { string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }) } diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 4116a457a3a..18f03ec57fb 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -60,7 +60,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable { } } - public func composedDisplayTitle(strings: PresentationStrings) -> String { + public func composedDisplayTitle(context: AccountContext, strings: PresentationStrings) -> String { switch self { case let .personName(firstName, lastName, phone): if !firstName.isEmpty { @@ -72,7 +72,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable { } else if !lastName.isEmpty { return lastName } else if !phone.isEmpty { - return formatPhoneNumber("+\(phone)") + return formatPhoneNumber(context: context, number: "+\(phone)") } else { return strings.User_DeletedAccount } @@ -395,7 +395,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo nameMaximumNumberOfLines = 2 } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(context: item.accountContext, strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var statusText: String = "" let statusColor: UIColor @@ -404,7 +404,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo switch item.mode { case .settings: if let phone = peer.phone, !phone.isEmpty { - statusText += formatPhoneNumber(phone) + statusText += formatPhoneNumber(context: item.accountContext, number: phone) } if let username = peer.addressName, !username.isEmpty { if !statusText.isEmpty { diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 1656908cb7a..3bd18d48523 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -22,6 +22,7 @@ public enum ItemListPeerActionItemColor { public class ItemListPeerActionItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let icon: UIImage? + let iconSignal: Signal? let title: String public let alwaysPlain: Bool let hasSeparator: Bool @@ -31,9 +32,10 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let action: (() -> Void)? - public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) { + public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal? = nil, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) { self.presentationData = presentationData self.icon = icon + self.iconSignal = iconSignal self.title = title self.alwaysPlain = alwaysPlain self.hasSeparator = hasSeparator @@ -114,6 +116,8 @@ class ItemListPeerActionItemNode: ListViewItemNode { private var item: ItemListPeerActionItem? + private let iconDisposable = MetaDisposable() + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -149,6 +153,10 @@ class ItemListPeerActionItemNode: ListViewItemNode { self.addSubnode(self.activateArea) } + deinit { + self.iconDisposable.dispose() + } + func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -171,7 +179,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { iconOffset = 1.0 verticalInset = 11.0 verticalOffset = 0.0 - leftInset = (item.icon == nil ? 16.0 : 59.0) + params.leftInset + leftInset = (item.icon == nil && item.iconSignal == nil ? 16.0 : 59.0) + params.leftInset case .peerList: iconOffset = 3.0 verticalInset = 14.0 @@ -232,6 +240,15 @@ class ItemListPeerActionItemNode: ListViewItemNode { strongSelf.iconNode.image = item.icon if let image = item.icon { transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0) + iconOffset, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) + } else if let iconSignal = item.iconSignal { + let imageSize = CGSize(width: 28.0, height: 28.0) + strongSelf.iconDisposable.set((iconSignal + |> deliverOnMainQueue).start(next: { [weak self] image in + if let strongSelf = self, let image { + strongSelf.iconNode.image = image + } + })) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - imageSize.width) / 2.0) + iconOffset, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)) } if strongSelf.backgroundNode.supernode == nil { diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index c997c00397c..1f437162bbd 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -484,7 +484,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } @@ -517,7 +517,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } } if fileUpdated, let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: resourceReference) + updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference) } } else { updatedImageSignal = .single({ _ in return nil }) diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index e2a17f9755c..59b5c920320 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -27,6 +27,8 @@ swift_library( "//submodules/AnimationUI:AnimationUI", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/AvatarNode", + "//submodules/TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index be05f8597b9..4911acb6730 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -5,12 +5,22 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import ShimmerEffect +import AvatarNode +import TelegramCore +import AccountContext + +private let avatarFont = avatarPlaceholderFont(size: 16.0) public enum ItemListDisclosureItemTitleColor { case primary case accent } +public enum ItemListDisclosureItemTitleFont { + case regular + case bold +} + public enum ItemListDisclosureStyle { case arrow case optionArrows @@ -31,11 +41,15 @@ public enum ItemListDisclosureLabelStyle { public class ItemListDisclosureItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let icon: UIImage? + let context: AccountContext? + let iconPeer: EnginePeer? let title: String let titleColor: ItemListDisclosureItemTitleColor + let titleFont: ItemListDisclosureItemTitleFont let enabled: Bool let label: String let labelStyle: ItemListDisclosureLabelStyle + let additionalDetailLabel: String? public let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle @@ -44,14 +58,18 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon + self.context = context + self.iconPeer = iconPeer self.title = title self.titleColor = titleColor + self.titleFont = titleFont self.enabled = enabled self.labelStyle = labelStyle self.label = label + self.additionalDetailLabel = additionalDetailLabel self.sectionId = sectionId self.style = style self.disclosureStyle = disclosureStyle @@ -115,9 +133,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { private let highlightedBackgroundNode: ASDisplayNode private let maskNode: ASImageNode + var avatarNode: AvatarNode? let iconNode: ASImageNode let titleNode: TextNode public let labelNode: TextNode + var additionalDetailLabelNode: TextNode? let arrowNode: ASImageNode let labelBadgeNode: ASImageNode let labelImageNode: ASImageNode @@ -213,6 +233,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) let currentItem = self.item @@ -284,8 +305,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let itemSeparatorColor: UIColor var leftInset = 16.0 + params.leftInset - if let _ = item.icon { + if item.icon != nil { leftInset += 43.0 + } else if item.iconPeer != nil { + leftInset += 46.0 } var additionalTextRightInset: CGFloat = 0.0 @@ -303,15 +326,31 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { titleColor = item.presentationData.theme.list.itemDisabledTextColor } - let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let titleFont: UIFont + let defaultLabelFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + switch item.titleFont { + case .regular: + titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + case .bold: + titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + } + + var maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset + if item.iconPeer != nil { + maxTitleWidth -= 12.0 + } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let labelFont: UIFont let labelBadgeColor: UIColor var labelConstrain: CGFloat = params.width - params.rightInset - leftInset - 40.0 - titleLayout.size.width - 10.0 + if item.iconPeer != nil { + labelConstrain -= 6.0 + } + switch item.labelStyle { case .badge: labelBadgeColor = item.presentationData.theme.list.itemCheckColors.foregroundColor @@ -322,22 +361,33 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { labelConstrain = params.width - params.rightInset - 40.0 - leftInset case let .coloredText(color): labelBadgeColor = color - labelFont = titleFont + labelFont = defaultLabelFont default: labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor - labelFont = titleFont + labelFont = defaultLabelFont } var multilineLabel = false if case .multilineDetailText = item.labelStyle { multilineLabel = true } - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + var additionalDetailLabelInfo: (TextNodeLayout, () -> TextNode)? + if let additionalDetailLabel = item.additionalDetailLabel { + additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + } + + let verticalInset: CGFloat + if item.iconPeer != nil { + verticalInset = 6.0 + } else { + verticalInset = 11.0 + } - let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 1.0 - let height: CGFloat + var height: CGFloat switch item.labelStyle { case .detailText: height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height @@ -346,6 +396,12 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { default: height = verticalInset * 2.0 + titleLayout.size.height } + if let additionalDetailLabelInfo = additionalDetailLabelInfo { + height += titleSpacing + additionalDetailLabelInfo.0.size.height + } + if item.iconPeer != nil { + height = max(height, 40.0 + verticalInset * 2.0) + } switch item.style { case .plain: @@ -394,6 +450,27 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.iconNode.removeFromSupernode() } + if let context = item.context, let iconPeer = item.iconPeer { + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarFont) + strongSelf.avatarNode = avatarNode + strongSelf.addSubnode(avatarNode) + } + let avatarSize: CGFloat = 40.0 + avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - avatarSize) / 2.0), y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + var clipStyle: AvatarNodeClipStyle = .round + if case let .channel(channel) = iconPeer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } + avatarNode.setPeer(context: context, theme: item.presentationData.theme, peer: iconPeer, clipStyle: clipStyle) + } else if let avatarNode = strongSelf.avatarNode { + strongSelf.avatarNode = nil + avatarNode.removeFromSupernode() + } + if let updateArrowImage = updateArrowImage { strongSelf.arrowNode.image = updateArrowImage } @@ -466,7 +543,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + var centralContentHeight: CGFloat = titleLayout.size.height + switch item.labelStyle { + case .detailText, .multilineDetailText: + centralContentHeight += titleSpacing + centralContentHeight += labelLayout.size.height + default: + break + } + if let additionalDetailLabelInfo { + centralContentHeight += titleSpacing + centralContentHeight += additionalDetailLabelInfo.0.size.height + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { @@ -486,14 +576,29 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let labelFrame: CGRect switch item.labelStyle { - case .badge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) - case .detailText, .multilineDetailText: - labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) - default: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: 11.0), size: labelLayout.size) + case .badge: + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1), size: labelLayout.size) + case .detailText, .multilineDetailText: + labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) + default: + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size) } strongSelf.labelNode.frame = labelFrame + + if let additionalDetailLabelInfo = additionalDetailLabelInfo { + let additionalDetailLabelNode = additionalDetailLabelInfo.1() + + if strongSelf.additionalDetailLabelNode !== additionalDetailLabelNode { + strongSelf.additionalDetailLabelNode?.removeFromSupernode() + strongSelf.additionalDetailLabelNode = additionalDetailLabelNode + strongSelf.addSubnode(additionalDetailLabelNode) + } + + additionalDetailLabelNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: additionalDetailLabelInfo.0.size) + } else if let additionalDetailLabelNode = strongSelf.additionalDetailLabelNode { + strongSelf.additionalDetailLabelNode = nil + additionalDetailLabelNode.removeFromSupernode() + } if case .textWithIcon = item.labelStyle { if let updatedLabelImage = updatedLabelImage { diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 7e0cd2563e5..07d6e201f7d 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData import SwitchNode +import AppBundle public enum ItemListSwitchItemNodeType { case regular @@ -19,6 +20,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { let type: ItemListSwitchItemNodeType let enableInteractiveChanges: Bool let enabled: Bool + let displayLocked: Bool let disableLeadingInset: Bool let maximumNumberOfLines: Int let noCorners: Bool @@ -28,7 +30,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { let activatedWhileDisabled: () -> Void public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, displayLocked: Bool = false, disableLeadingInset: Bool = false, maximumNumberOfLines: Int = 1, noCorners: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void, activatedWhileDisabled: @escaping () -> Void = {}, tag: ItemListItemTag? = nil) { self.presentationData = presentationData self.icon = icon self.title = title @@ -36,6 +38,7 @@ public class ItemListSwitchItem: ListViewItem, ItemListItem { self.type = type self.enableInteractiveChanges = enableInteractiveChanges self.enabled = enabled + self.displayLocked = displayLocked self.disableLeadingInset = disableLeadingInset self.maximumNumberOfLines = maximumNumberOfLines self.noCorners = noCorners @@ -128,6 +131,8 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { private let switchGestureNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? + private var lockedIconNode: ASImageNode? + private let activateArea: AccessibilityAreaNode private var item: ItemListSwitchItem? @@ -245,7 +250,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { insets.bottom = 0.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 64.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0) @@ -405,6 +410,36 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.switchGestureNode.isHidden = item.enableInteractiveChanges && item.enabled + if item.displayLocked { + var updateLockedIconImage = false + if let _ = updatedTheme { + updateLockedIconImage = true + } + + let lockedIconNode: ASImageNode + if let current = strongSelf.lockedIconNode { + lockedIconNode = current + } else { + updateLockedIconImage = true + lockedIconNode = ASImageNode() + strongSelf.lockedIconNode = lockedIconNode + strongSelf.insertSubnode(lockedIconNode, aboveSubnode: strongSelf.switchNode) + } + + if updateLockedIconImage, let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: item.presentationData.theme.list.itemSecondaryTextColor) { + lockedIconNode.image = image + } + + let switchFrame = strongSelf.switchNode.frame + + if let icon = lockedIconNode.image { + lockedIconNode.frame = CGRect(origin: CGPoint(x: switchFrame.minX + 10.0 + UIScreenPixel, y: switchFrame.minY + 9.0), size: icon.size) + } + } else if let lockedIconNode = strongSelf.lockedIconNode { + strongSelf.lockedIconNode = nil + lockedIconNode.removeFromSupernode() + } + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h index 9ced7022034..6a96f9b9ece 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponents.h @@ -211,7 +211,6 @@ #import #import #import -#import #import #import #import @@ -228,10 +227,6 @@ #import #import #import -#import -#import -#import -#import #import #import #import diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h index 311592de921..a7c7db0a099 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/LegacyComponentsContext.h @@ -54,6 +54,10 @@ typedef enum { - (void)forceStatusBarAppearanceUpdate; - (bool)prefersLightStatusBar; +- (void)lockPortrait; +- (void)unlockPortrait; +- (void)disableInteractiveKeyboardGesture; + - (TGNavigationBarPallete *)navigationBarPallete; - (TGMenuSheetPallete *)menuSheetPallete; - (TGMenuSheetPallete *)darkMenuSheetPallete; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h index 248b9594d83..38d1fdfa9ff 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h @@ -34,6 +34,7 @@ @property (nonatomic) bool hasSchedule; @property (nonatomic) bool reminder; @property (nonatomic) bool forum; +@property (nonatomic) bool isSuggesting; @property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @@ -42,8 +43,8 @@ @property (nonatomic, copy) void (^cameraPressed)(TGAttachmentCameraView *cameraView); @property (nonatomic, copy) void (^sendPressed)(TGMediaAsset *currentItem, bool asFiles, bool silentPosting, int32_t scheduleTime, bool isFromPicker); -@property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image); -@property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, id asset, TGVideoEditAdjustments *adjustments); +@property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image, void(^commit)(void)); +@property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, id asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)); @property (nonatomic, copy) void (^editorOpened)(void); @property (nonatomic, copy) void (^editorClosed)(void); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h index f587ebab6f0..3e21657bdce 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h @@ -64,6 +64,10 @@ typedef enum @property (nonatomic, assign) bool hasSilentPosting; @property (nonatomic, assign) bool hasSchedule; @property (nonatomic, assign) bool reminder; + +@property (nonatomic, assign) bool forum; +@property (nonatomic, assign) bool isSuggesting; + @property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @@ -73,9 +77,9 @@ typedef enum @property (nonatomic, strong) NSString *recipientName; @property (nonatomic, copy) NSDictionary *(^descriptionGenerator)(id, NSAttributedString *, NSString *, NSString *); -@property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image); +@property (nonatomic, copy) void (^avatarCompletionBlock)(UIImage *image, void(^commit)(void)); @property (nonatomic, copy) void (^completionBlock)(NSArray *signals, bool silentPosting, int32_t scheduleTime); -@property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments); +@property (nonatomic, copy) void (^avatarVideoCompletionBlock)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)); @property (nonatomic, copy) void (^singleCompletionBlock)(id item, TGMediaEditingContext *editingContext); @property (nonatomic, copy) void (^dismissalBlock)(void); @property (nonatomic, copy) void (^selectionBlock)(TGMediaAsset *asset, UIImage *); @@ -96,8 +100,8 @@ typedef enum - (NSArray *)resultSignalsWithCurrentItem:(TGMediaAsset *)currentItem descriptionGenerator:(id (^)(id, NSAttributedString *, NSString *, NSString *))descriptionGenerator; -- (void)completeWithAvatarImage:(UIImage *)image; -- (void)completeWithAvatarVideo:(id)asset adjustments:(TGVideoEditAdjustments *)adjustments image:(UIImage *)image; +- (void)completeWithAvatarImage:(UIImage *)image commit:(void(^)(void))commit; +- (void)completeWithAvatarVideo:(id)asset adjustments:(TGVideoEditAdjustments *)adjustments image:(UIImage *)image commit:(void(^)(void))commit; - (void)completeWithCurrentItem:(TGMediaAsset *)currentItem silentPosting:(bool)silentPosting scheduleTime:(int32_t)scheduleTime; - (void)dismiss; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h index d350d22075a..6c69dadda9b 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAvatarMenuMixin.h @@ -15,6 +15,9 @@ typedef void (^TGMediaAvatarPresentImpl)(id, void (^)(U @property (nonatomic, assign) bool forceDark; +@property (nonatomic, copy) void (^willFinishWithImage)(UIImage *image, void (^)(void)); +@property (nonatomic, copy) void (^willFinishWithVideo)(UIImage *image, void (^)(void)); + @property (nonatomic, copy) void (^didFinishWithImage)(UIImage *image); @property (nonatomic, copy) void (^didFinishWithVideo)(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments); @property (nonatomic, copy) void (^didFinishWithDelete)(void); @@ -27,7 +30,7 @@ typedef void (^TGMediaAvatarPresentImpl)(id, void (^)(U - (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasDeleteButton:(bool)hasDeleteButton saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; - (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasDeleteButton:(bool)hasDeleteButton personalPhoto:(bool)personalPhoto saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; -- (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasSearchButton:(bool)hasSearchButton hasDeleteButton:(bool)hasDeleteButton hasViewButton:(bool)hasViewButton personalPhoto:(bool)personalPhoto isVideo:(bool)isVideo saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia signup:(bool)signup forum:(bool)forum; +- (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasSearchButton:(bool)hasSearchButton hasDeleteButton:(bool)hasDeleteButton hasViewButton:(bool)hasViewButton personalPhoto:(bool)personalPhoto isVideo:(bool)isVideo saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia signup:(bool)signup forum:(bool)forum title:(NSString *)title isSuggesting:(bool)isSuggesting; - (TGMenuSheetController *)present; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 1975a395680..7871f47ecc3 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -86,9 +86,15 @@ - (void)setTimer:(NSNumber *)timer forItem:(NSObject *)item; - (SSignal *)timersUpdatedSignal; +- (bool)spoilerForItem:(NSObject *)item; +- (SSignal *)spoilerSignalForItem:(NSObject *)item; +- (SSignal *)spoilerSignalForIdentifier:(NSString *)identifier; +- (void)setSpoiler:(bool)spoiler forItem:(NSObject *)item; +- (SSignal *)spoilersUpdatedSignal; + - (UIImage *)paintingImageForItem:(NSObject *)item; - (UIImage *)stillPaintingImageForItem:(NSObject *)item; -- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)image forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video; +- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video; - (void)clearPaintingData; - (SSignal *)facesForItem:(NSObject *)item; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h index 6d8c8b9e5c0..6e48ddfac52 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h @@ -30,6 +30,10 @@ @property (nonatomic, assign) bool hasSilentPosting; @property (nonatomic, assign) bool hasSchedule; @property (nonatomic, assign) bool reminder; + +@property (nonatomic, assign) bool forum; +@property (nonatomic, assign) bool isSuggesting; + @property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h index fefbd2433bf..e35ddc83f28 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h @@ -3,8 +3,8 @@ #import #import -@class TGPhotoEntitiesContainerView; @protocol TGMediaEditableItem; +@protocol TGPhotoDrawingEntitiesView; @interface TGMediaPickerGalleryVideoItemView : TGModernGalleryItemView @@ -34,7 +34,7 @@ - (UIImage *)screenImage; - (UIImage *)transitionImage; - (CGRect)editorTransitionViewRect; -- (TGPhotoEntitiesContainerView *)entitiesView; +- (UIView *)entitiesView; - (id)editableMediaItem; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintUndoManager.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintUndoManager.h deleted file mode 100644 index ee49b47ebaa..00000000000 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintUndoManager.h +++ /dev/null @@ -1,21 +0,0 @@ -#import - -@class TGPainting; -@class TGPhotoEntitiesContainerView; - -@interface TGPaintUndoManager : NSObject - -@property (nonatomic, weak) TGPainting *painting; -@property (nonatomic, weak) TGPhotoEntitiesContainerView *entitiesContainer; - -@property (nonatomic, copy) void (^historyChanged)(void); - -@property (nonatomic, readonly) bool canUndo; -- (void)registerUndoWithUUID:(NSInteger)uuid block:(void (^)(TGPainting *, TGPhotoEntitiesContainerView *, NSInteger))block; -- (void)unregisterUndoWithUUID:(NSInteger)uuid; - -- (void)undo; - -- (void)reset; - -@end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h index 9b7cf065db1..f3cf8161ef9 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h @@ -1,28 +1,26 @@ #import #import -@class TGPaintUndoManager; @class TGMediaEditingContext; @protocol TGMediaEditableItem; @interface TGPaintingData : NSObject @property (nonatomic, readonly) NSString *imagePath; -@property (nonatomic, readonly) NSString *dataPath; -@property (nonatomic, readonly) NSArray *entities; -@property (nonatomic, readonly) TGPaintUndoManager *undoManager; -@property (nonatomic, readonly) NSArray *stickers; -@property (nonatomic, readonly) NSData *data; -@property (nonatomic, readonly) UIImage *image; +@property (nonatomic, readonly) NSData *drawingData; +@property (nonatomic, readonly) NSData *entitiesData; +@property (nonatomic, readonly) UIImage *image; @property (nonatomic, readonly) UIImage *stillImage; +@property (nonatomic, readonly) NSArray *stickers; + @property (nonatomic, readonly) bool hasAnimation; -+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager; ++ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers; -+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities; ++ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers; + (instancetype)dataWithPaintingImagePath:(NSString *)imagePath; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoAvatarCropView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoAvatarCropView.h index 871b8f0f4a3..dd61efecaf8 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoAvatarCropView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoAvatarCropView.h @@ -1,7 +1,7 @@ #import @class PGPhotoEditorView; -@class TGPhotoEntitiesContainerView; +@protocol TGPhotoDrawingEntitiesView; @interface TGPhotoAvatarCropView : UIView @@ -23,7 +23,7 @@ @property (nonatomic, readonly) bool isTracking; @property (nonatomic, readonly) bool isAnimating; -- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square; +- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView *)fullEntitiesView square:(bool)square; - (void)setSnapshotImage:(UIImage *)image; - (void)setSnapshotView:(UIView *)snapshotView; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h index 0642a37cb3f..722b36b1a9f 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorController.h @@ -12,7 +12,7 @@ @class AVPlayer; @protocol TGPhotoPaintStickersContext; -@class TGPhotoEntitiesContainerView; +@protocol TGPhotoDrawingEntitiesView; typedef enum { TGPhotoEditorControllerGenericIntent = 0, @@ -21,7 +21,9 @@ typedef enum { TGPhotoEditorControllerFromCameraIntent = (1 << 2), TGPhotoEditorControllerWebIntent = (1 << 3), TGPhotoEditorControllerVideoIntent = (1 << 4), - TGPhotoEditorControllerForumAvatarIntent = (1 << 5) + TGPhotoEditorControllerForumAvatarIntent = (1 << 5), + TGPhotoEditorControllerSuggestedAvatarIntent = (1 << 6), + TGPhotoEditorControllerSuggestingAvatarIntent = (1 << 7) } TGPhotoEditorControllerIntent; @interface TGPhotoEditorController : TGOverlayController @@ -51,17 +53,19 @@ typedef enum { @property (nonatomic, copy) void (^willFinishEditing)(id adjustments, id temporaryRep, bool hasChanges); @property (nonatomic, copy) void (^didFinishRenderingFullSizeImage)(UIImage *fullSizeImage); -@property (nonatomic, copy) void (^didFinishEditing)(id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges); -@property (nonatomic, copy) void (^didFinishEditingVideo)(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges); +@property (nonatomic, copy) void (^didFinishEditing)(id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)); +@property (nonatomic, copy) void (^didFinishEditingVideo)(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)); @property (nonatomic, assign) bool skipInitialTransition; @property (nonatomic, assign) bool dontHideStatusBar; @property (nonatomic, strong) PGCameraShotMetadata *metadata; @property (nonatomic, strong) NSArray *faces; +@property (nonatomic, strong) NSString *senderName; + @property (nonatomic, strong) AVPlayer *player; -@property (nonatomic, strong) TGPhotoEntitiesContainerView *entitiesView; +@property (nonatomic, strong) UIView *entitiesView; - (instancetype)initWithContext:(id)context item:(id)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h index 0646c13aad5..bd87dbf0e8a 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorTabController.h @@ -4,6 +4,12 @@ @protocol TGMediaEditAdjustments; +@protocol TGPhotoEditorTabProtocol + + + +@end + @interface TGPhotoEditorTabController : TGViewController { bool _dismissing; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintEntityView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintEntityView.h index 49601b8e275..828132bc33f 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintEntityView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintEntityView.h @@ -2,7 +2,6 @@ @class TGPhotoPaintEntity; @class TGPhotoPaintEntitySelectionView; -@class TGPaintUndoManager; @interface UIView (PixelColor) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index e2e38a1bf90..17e3dbad066 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -7,64 +7,112 @@ @protocol TGPhotoPaintEntityRenderer -- (void)entitiesForTime:(CMTime)time fps:(NSInteger)fps size:(CGSize)size completion:(void(^)(NSArray *))completion; +- (void)entitiesForTime:(CMTime)time fps:(NSInteger)fps size:(CGSize)size completion:(void(^_Nonnull)(NSArray * _Nonnull))completion; @end -@protocol TGPhotoPaintStickerRenderView +@protocol TGPhotoSolidRoundedButtonView -@property (nonatomic, copy) void(^started)(double); +- (void)updateWidth:(CGFloat)width; + +@end + + +@protocol TGCaptionPanelView + +@property (nonatomic, readonly) UIView * _Nonnull view; + +- (NSAttributedString * _Nonnull)caption; +- (void)setCaption:(NSAttributedString * _Nullable)caption; +- (void)dismissInput; + +@property (nonatomic, copy) void(^ _Nullable sendPressed)(NSAttributedString * _Nullable string); +@property (nonatomic, copy) void(^ _Nullable focusUpdated)(BOOL focused); +@property (nonatomic, copy) void(^ _Nullable heightUpdated)(BOOL animated); + +- (CGFloat)updateLayoutSize:(CGSize)size sideInset:(CGFloat)sideInset; +- (CGFloat)baseHeight; + +@end + + +@protocol TGPhotoDrawingView + +@property (nonatomic, readonly) BOOL isTracking; +@property (nonatomic, assign) CGSize screenSize; + +@property (nonatomic, copy) void(^ _Nonnull zoomOut)(void); + +- (void)updateZoomScale:(CGFloat)scale; + +- (void)setupWithDrawingData:(NSData * _Nullable)drawingData; + +@end + +@protocol TGPhotoDrawingEntitiesView + +@property (nonatomic, copy) CGPoint (^ _Nonnull getEntityCenterPosition)(void); +@property (nonatomic, copy) CGFloat (^ _Nonnull getEntityInitialRotation)(void); + +@property (nonatomic, copy) void(^ _Nonnull hasSelectionChanged)(bool); +@property (nonatomic, readonly) BOOL hasSelection; -- (void)setIsVisible:(bool)isVisible; -- (void)seekTo:(double)timestamp; - (void)play; - (void)pause; +- (void)seekTo:(double)timestamp; - (void)resetToStart; +- (void)updateVisibility:(BOOL)visibility; +- (void)clearSelection; +- (void)onZoom; -- (void)playFromFrame:(NSInteger)frameIndex; -- (void)copyStickerView:(NSObject *)view; +- (void)handlePinch:(UIPinchGestureRecognizer * _Nonnull)gestureRecognizer; +- (void)handleRotate:(UIRotationGestureRecognizer * _Nonnull)gestureRecognizer; -- (int64_t)documentId; -- (UIImage *)image; +- (void)setupWithEntitiesData:(NSData * _Nullable)entitiesData; @end -@protocol TGPhotoPaintStickersScreen +@protocol TGPhotoDrawingInterfaceController -@property (nonatomic, copy) void(^screenDidAppear)(void); -@property (nonatomic, copy) void(^screenWillDisappear)(void); +@property (nonatomic, copy) void(^ _Nonnull requestDismiss)(void); +@property (nonatomic, copy) void(^ _Nonnull requestApply)(void); +@property (nonatomic, copy) UIImage * _Nullable(^ _Nonnull getCurrentImage)(void); +@property (nonatomic, copy) void(^ _Nonnull updateVideoPlayback)(bool); -- (void)restore; -- (void)invalidate; +- (TGPaintingData * _Nullable)generateResultData; +- (void)animateOut:(void(^_Nonnull)(void))completion; -@end - -@protocol TGCaptionPanelView +- (void)adapterContainerLayoutUpdatedSize:(CGSize)size + intrinsicInsets:(UIEdgeInsets)intrinsicInsets + safeInsets:(UIEdgeInsets)safeInsets + statusBarHeight:(CGFloat)statusBarHeight + inputHeight:(CGFloat)inputHeight + orientation:(UIInterfaceOrientation)orientation + isRegular:(bool)isRegular + animated:(BOOL)animated; -@property (nonatomic, readonly) UIView *view; +@end -- (NSAttributedString *)caption; -- (void)setCaption:(NSAttributedString *)caption; -- (void)dismissInput; -@property (nonatomic, copy) void(^sendPressed)(NSAttributedString *string); -@property (nonatomic, copy) void(^focusUpdated)(BOOL focused); -@property (nonatomic, copy) void(^heightUpdated)(BOOL animated); +@protocol TGPhotoDrawingAdapter -- (CGFloat)updateLayoutSize:(CGSize)size sideInset:(CGFloat)sideInset; -- (CGFloat)baseHeight; +@property (nonatomic, readonly) id _Nonnull drawingView; +@property (nonatomic, readonly) id _Nonnull drawingEntitiesView; +@property (nonatomic, readonly) UIView * _Nonnull selectionContainerView; +@property (nonatomic, readonly) UIView * _Nonnull contentWrapperView; +@property (nonatomic, readonly) id _Nonnull interfaceController; @end + @protocol TGPhotoPaintStickersContext -- (int64_t)documentIdForDocument:(id)document; -- (TGStickerMaskDescription *)maskDescriptionForDocument:(id)document; +@property (nonatomic, copy) id _Nullable(^ _Nullable captionPanelView)(void); -- (UIView *)stickerViewForDocument:(id)document; -@property (nonatomic, copy) id(^presentStickersController)(void(^)(id, bool, UIView *, CGRect)); +- (UIView *_Nonnull)solidRoundedButton:(NSString *_Nonnull)title action:(void(^_Nonnull)(void))action; +- (id _Nonnull)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize isVideo:(bool)isVideo isAvatar:(bool)isAvatar entitiesView:(UIView * _Nullable)entitiesView; -@property (nonatomic, copy) id(^captionPanelView)(void); +- (UIView * _Nonnull)drawingEntitiesViewWithSize:(CGSize)size; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h index 2cde313c367..d7b28a8e1d6 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintTextEntity.h @@ -23,3 +23,10 @@ typedef enum { - (instancetype)initWithText:(NSString *)text font:(TGPhotoPaintFont *)font swatch:(TGPaintSwatch *)swatch baseFontSize:(CGFloat)baseFontSize maxWidth:(CGFloat)maxWidth style:(TGPhotoPaintTextEntityStyle)style; @end + + +@interface TGPhotoPaintStaticEntity : TGPhotoPaintEntity + +@property (nonatomic, strong) UIImage *renderImage; + +@end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index 0d275e10d88..7426a814abe 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -65,7 +65,9 @@ typedef enum - (void)setEditButtonsHighlighted:(TGPhotoEditorTab)buttons; - (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons; +- (void)setCenterButtonsHidden:(bool)hidden animated:(bool)animated; - (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated; +- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated; @property (nonatomic, readonly) TGPhotoEditorTab currentTabs; - (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h index f1012f7edd0..737380548df 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h @@ -2,7 +2,7 @@ @interface TGPhotoVideoEditor : NSObject -+ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; ++ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; + (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed; diff --git a/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m b/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m index bd47f4a6822..1c614e3b53a 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m +++ b/submodules/LegacyComponents/Sources/PGPhotoCustomFilterPass.m @@ -98,7 +98,6 @@ - (void)addTextureWithImage:(UIImage *)image textureIndex:(NSInteger)textureInde GLubyte *imageData = NULL; CFDataRef dataFromImageDataProvider = NULL; - GLenum format = GL_BGRA; if (!redrawNeeded) { @@ -136,10 +135,6 @@ - (void)addTextureWithImage:(UIImage *)image textureIndex:(NSInteger)textureInde alphaInfo != kCGImageAlphaNoneSkipLast) { redrawNeeded = true; - } else - { - /* Can access directly using GL_RGBA pixel format */ - format = GL_RGBA; } } } diff --git a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m index f06d3cdc635..10fd7273cbc 100644 --- a/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m +++ b/submodules/LegacyComponents/Sources/TGAttachmentCarouselItemView.m @@ -915,6 +915,9 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa if (_forum) { intent |= TGPhotoEditorControllerForumAvatarIntent; } + if (_isSuggesting) { + intent |= TGPhotoEditorControllerSuggestingAvatarIntent; + } TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:intent adjustments:nil caption:nil screenImage:thumbnailImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; controller.editingContext = _editingContext; @@ -933,11 +936,10 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa }; __weak TGPhotoEditorController *weakController = controller; - controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges) + controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; - __strong TGAttachmentCarouselItemView *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -970,13 +972,13 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa } } if (strongSelf.avatarVideoCompletionBlock != nil) - strongSelf.avatarVideoCompletionBlock(previewImage, [NSURL fileURLWithPath:filePath], videoAdjustments); + strongSelf.avatarVideoCompletionBlock(previewImage, [NSURL fileURLWithPath:filePath], videoAdjustments, commit); } else { if (strongSelf.avatarCompletionBlock != nil) - strongSelf.avatarCompletionBlock(resultImage); + strongSelf.avatarCompletionBlock(resultImage, commit); } }; - controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) { + controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; @@ -989,7 +991,7 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa return; if (strongSelf.avatarVideoCompletionBlock != nil) - strongSelf.avatarVideoCompletionBlock(resultImage, asset, adjustments); + strongSelf.avatarVideoCompletionBlock(resultImage, asset, adjustments, commit); }; controller.requestThumbnailImage = ^(id editableItem) { diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index e17b86a3e05..5eed9233165 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -2014,7 +2014,7 @@ - (void)presentPhotoResultControllerWithImage:(id)input met return strongSelf->_previewView; }; - controller.didFinishEditing = ^(PGPhotoEditorValues *editorValues, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges) + controller.didFinishEditing = ^(PGPhotoEditorValues *editorValues, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; @@ -2064,10 +2064,12 @@ - (void)presentPhotoResultControllerWithImage:(id)input met [strongController updateStatusBarAppearanceForDismiss]; [strongSelf _dismissTransitionForResultController:(TGOverlayController *)strongController]; } + + commit(); }); }; - controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) { + controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; @@ -2086,6 +2088,8 @@ - (void)presentPhotoResultControllerWithImage:(id)input met [strongController updateStatusBarAppearanceForDismiss]; [strongSelf _dismissTransitionForResultController:(TGOverlayController *)strongController]; } + + commit(); }); }; @@ -2965,14 +2969,7 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (adjustments.paintingData.stickers.count > 0) dict[@"stickers"] = adjustments.paintingData.stickers; - bool animated = false; - for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) { - if (entity.animated) { - animated = true; - break; - } - } - + bool animated = adjustments.paintingData.hasAnimation; if (animated) { dict[@"isAnimation"] = @true; if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) { diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 1b933d71836..74f4a95599a 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -260,6 +260,8 @@ + (instancetype)controllerWithContext:(id)context asset pickerController.hasSilentPosting = strongController.hasSilentPosting; pickerController.hasSchedule = strongController.hasSchedule; pickerController.reminder = strongController.reminder; + pickerController.forum = strongController.forum; + pickerController.isSuggesting = strongController.isSuggesting; pickerController.presentScheduleController = strongController.presentScheduleController; pickerController.presentTimerController = strongController.presentTimerController; [strongController pushViewController:pickerController animated:true]; @@ -363,6 +365,16 @@ - (void)setReminder:(bool)reminder self.pickerController.reminder = reminder; } +- (void)setForum:(bool)forum { + _forum = forum; + self.pickerController.forum = forum; +} + +- (void)setIsSuggesting:(bool)isSuggesting { + _isSuggesting = isSuggesting; + self.pickerController.isSuggesting = isSuggesting; +} + - (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t)))presentScheduleController { _presentScheduleController = [presentScheduleController copy]; self.pickerController.presentScheduleController = presentScheduleController; @@ -804,16 +816,16 @@ - (void)viewDidLayoutSubviews #pragma mark - -- (void)completeWithAvatarImage:(UIImage *)image +- (void)completeWithAvatarImage:(UIImage *)image commit:(void(^)(void))commit { if (self.avatarCompletionBlock != nil) - self.avatarCompletionBlock(image); + self.avatarCompletionBlock(image, commit); } -- (void)completeWithAvatarVideo:(AVAsset *)asset adjustments:(TGVideoEditAdjustments *)adjustments image:(UIImage *)image +- (void)completeWithAvatarVideo:(AVAsset *)asset adjustments:(TGVideoEditAdjustments *)adjustments image:(UIImage *)image commit:(void(^)(void))commit { if (self.avatarVideoCompletionBlock != nil) - self.avatarVideoCompletionBlock(image, asset, adjustments); + self.avatarVideoCompletionBlock(image, asset, adjustments, commit); } - (void)completeWithCurrentItem:(TGMediaAsset *)currentItem silentPosting:(bool)silentPosting scheduleTime:(int32_t)scheduleTime @@ -929,10 +941,8 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti grouping = false; } } - for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) { - if (entity.animated) { - grouping = true; - } + if (adjustments.paintingData.hasAnimation) { + grouping = false; } } } @@ -957,6 +967,8 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti } } + bool spoiler = [editingContext spoilerForItem:item]; + switch (asset.type) { case TGMediaAssetPhotoType: @@ -1029,6 +1041,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }]; @@ -1105,6 +1121,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }]; @@ -1149,14 +1169,7 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (adjustments.paintingData.stickers.count > 0) dict[@"stickers"] = adjustments.paintingData.stickers; - bool animated = false; - for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) { - if (entity.animated) { - animated = true; - break; - } - } - + bool animated = adjustments.paintingData.hasAnimation; if (animated) { dict[@"isAnimation"] = @true; if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) { @@ -1188,6 +1201,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }] catch:^SSignal *(__unused id error) @@ -1228,6 +1245,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (groupedId != nil) dict[@"groupedId"] = groupedId; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }]]; @@ -1297,6 +1318,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti else if (groupedId != nil && !hasAnyTimers) dict[@"groupedId"] = groupedId; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }]]; @@ -1374,6 +1399,10 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti if (timer != nil) dict[@"timer"] = timer; + if (spoiler) { + dict[@"spoiler"] = @true; + } + id generatedItem = descriptionGenerator(dict, caption, nil, asset.identifier); return generatedItem; }]]; @@ -1387,8 +1416,7 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti break; } - if (groupedId != nil && i == 10) - { + if (groupedId != nil && i == 10) { i = 0; groupedId = @([self generateGroupedId]); } @@ -1423,10 +1451,8 @@ + (NSArray *)pasteboardResultSignalsForSelectionContext:(TGMediaSelectionContext grouping = false; } } - for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) { - if (entity.animated) { - grouping = true; - } + if (adjustments.paintingData.hasAnimation) { + grouping = false; } } } diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m index 7c8b36868cd..d6d9d2420ba 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsPickerController.m @@ -435,6 +435,14 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa intent = TGPhotoEditorControllerSignupAvatarIntent; } + if (self.forum) { + intent |= TGPhotoEditorControllerForumAvatarIntent; + } + + if (self.isSuggesting) { + intent |= TGPhotoEditorControllerSuggestingAvatarIntent; + } + id editableItem = asset; if (asset.type == TGMediaAssetGifType) { editableItem = [[TGCameraCapturedVideo alloc] initWithAsset:asset livePhoto:false]; @@ -451,7 +459,7 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa [[strongSelf->_assetsLibrary saveAssetWithImage:resultImage] startWithNext:nil]; }; - controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges) + controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; @@ -483,12 +491,12 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa previewImage = thumbnailImage; } } - [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarVideo:[NSURL fileURLWithPath:filePath] adjustments:videoAdjustments image:previewImage]; + [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarVideo:[NSURL fileURLWithPath:filePath] adjustments:videoAdjustments image:previewImage commit:commit]; } else { - [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarImage:resultImage]; + [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarImage:resultImage commit:commit]; } }; - controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) { + controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (!hasChanges) return; @@ -496,7 +504,7 @@ - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPa if (strongSelf == nil) return; - [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarVideo:asset adjustments:adjustments image:resultImage]; + [(TGMediaAssetsController *)strongSelf.navigationController completeWithAvatarVideo:asset adjustments:adjustments image:resultImage commit:commit]; }; controller.requestThumbnailImage = ^(id editableItem) { diff --git a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m index 01855f1ffb6..2b1c3c8771d 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m @@ -27,6 +27,8 @@ @interface TGMediaAvatarMenuMixin () bool _signup; bool _isVideo; bool _forum; + NSString *_title; + bool _isSuggesting; } @end @@ -39,10 +41,10 @@ - (instancetype)initWithContext:(id)context parentContr - (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasDeleteButton:(bool)hasDeleteButton personalPhoto:(bool)personalPhoto saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia { - return [self initWithContext:context parentController:parentController hasSearchButton:false hasDeleteButton:hasDeleteButton hasViewButton:false personalPhoto:personalPhoto isVideo:false saveEditedPhotos:saveEditedPhotos saveCapturedMedia:saveCapturedMedia signup:false forum: false]; + return [self initWithContext:context parentController:parentController hasSearchButton:false hasDeleteButton:hasDeleteButton hasViewButton:false personalPhoto:personalPhoto isVideo:false saveEditedPhotos:saveEditedPhotos saveCapturedMedia:saveCapturedMedia signup:false forum:false title:nil isSuggesting:false]; } -- (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasSearchButton:(bool)hasSearchButton hasDeleteButton:(bool)hasDeleteButton hasViewButton:(bool)hasViewButton personalPhoto:(bool)personalPhoto isVideo:(bool)isVideo saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia signup:(bool)signup forum:(bool)forum +- (instancetype)initWithContext:(id)context parentController:(TGViewController *)parentController hasSearchButton:(bool)hasSearchButton hasDeleteButton:(bool)hasDeleteButton hasViewButton:(bool)hasViewButton personalPhoto:(bool)personalPhoto isVideo:(bool)isVideo saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia signup:(bool)signup forum:(bool)forum title:(NSString *)title isSuggesting:(bool)isSuggesting { self = [super init]; if (self != nil) @@ -58,6 +60,8 @@ - (instancetype)initWithContext:(id)context parentContr _isVideo = isVideo; _signup = signup; _forum = forum; + _title = title; + _isSuggesting = isSuggesting; } return self; } @@ -92,7 +96,12 @@ - (TGMenuSheetController *)_presentAvatarMenu NSMutableArray *itemViews = [[NSMutableArray alloc] init]; + if (_title.length > 0) { + [itemViews addObject:[[TGMenuSheetTitleItemView alloc] initWithTitle:nil subtitle:_title solidSubtitle:false]]; + } + TGAttachmentCarouselItemView *carouselItem = [[TGAttachmentCarouselItemView alloc] initWithContext:_context camera:true selfPortrait:_personalPhoto forProfilePhoto:true assetType:_signup ? TGMediaAssetPhotoType : TGMediaAssetAnyType saveEditedPhotos:_saveEditedPhotos allowGrouping:false]; + carouselItem.isSuggesting = _isSuggesting; carouselItem.forum = _forum; carouselItem.stickersContext = _stickersContext; carouselItem.parentController = _parentController; @@ -112,7 +121,7 @@ - (TGMenuSheetController *)_presentAvatarMenu [strongSelf _displayCameraWithView:cameraView menuController:strongController]; }; - carouselItem.avatarCompletionBlock = ^(UIImage *resultImage) + carouselItem.avatarCompletionBlock = ^(UIImage *resultImage, void(^commit)(void)) { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) @@ -122,12 +131,25 @@ - (TGMenuSheetController *)_presentAvatarMenu if (strongController == nil) return; - if (strongSelf.didFinishWithImage != nil) - strongSelf.didFinishWithImage(resultImage); - - [strongController dismissAnimated:false]; + if (strongSelf.willFinishWithImage != nil) { + strongSelf.willFinishWithImage(resultImage, ^{ + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + commit(); + + [strongController dismissAnimated:false]; + }); + } else { + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + commit(); + + [strongController dismissAnimated:false]; + } }; - carouselItem.avatarVideoCompletionBlock = ^(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments) { + carouselItem.avatarVideoCompletionBlock = ^(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)) { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -136,10 +158,23 @@ - (TGMenuSheetController *)_presentAvatarMenu if (strongController == nil) return; - if (strongSelf.didFinishWithVideo != nil) - strongSelf.didFinishWithVideo(image, asset, adjustments); - - [strongController dismissAnimated:false]; + if (strongSelf.willFinishWithVideo != nil) { + strongSelf.willFinishWithVideo(image, ^{ + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + + [strongController dismissAnimated:false]; + }); + } else { + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + + [strongController dismissAnimated:false]; + } }; [itemViews addObject:carouselItem]; @@ -158,24 +193,24 @@ - (TGMenuSheetController *)_presentAvatarMenu }]; [itemViews addObject:galleryItem]; - if (_hasSearchButton) - { - TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ - { - __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - __strong TGMenuSheetController *strongController = weakController; - if (strongController == nil) - return; - - [strongController dismissAnimated:true]; - if (strongSelf != nil) - strongSelf.requestSearchController(nil); - }]; - [itemViews addObject:viewItem]; - } +// if (_hasSearchButton) +// { +// TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ +// { +// __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// __strong TGMenuSheetController *strongController = weakController; +// if (strongController == nil) +// return; +// +// [strongController dismissAnimated:true]; +// if (strongSelf != nil) +// strongSelf.requestSearchController(nil); +// }]; +// [itemViews addObject:viewItem]; +// } if (_hasViewButton) { @@ -314,10 +349,19 @@ - (void)_displayCameraWithView:(TGAttachmentCameraView *)cameraView menuControll if (strongSelf == nil) return; - if (strongSelf.didFinishWithImage != nil) - strongSelf.didFinishWithImage(resultImage); - - [menuController dismissAnimated:false]; + if (strongSelf.willFinishWithImage != nil) { + strongSelf.willFinishWithImage(resultImage, ^{ + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + [menuController dismissAnimated:false]; + }); + } else { + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + [menuController dismissAnimated:false]; + } }; controller.finishedWithVideo = ^(__unused TGOverlayController *controller, NSURL *url, UIImage *previewImage, __unused NSTimeInterval duration, __unused CGSize dimensions, TGVideoEditAdjustments *adjustments, __unused NSAttributedString *caption, __unused NSArray *stickers, __unused NSNumber *timer){ @@ -426,30 +470,62 @@ - (void)_displayMediaPicker TGMediaAssetsController *controller = [TGMediaAssetsController controllerWithContext:context assetGroup:group intent:strongSelf->_signup ? TGMediaAssetsControllerSetSignupProfilePhotoIntent : TGMediaAssetsControllerSetProfilePhotoIntent recipientName:nil saveEditedPhotos:strongSelf->_saveEditedPhotos allowGrouping:false selectionLimit:10]; __weak TGMediaAssetsController *weakController = controller; controller.stickersContext = _stickersContext; - controller.avatarCompletionBlock = ^(UIImage *resultImage) - { + controller.forum = _forum; + controller.isSuggesting = _isSuggesting; + controller.avatarCompletionBlock = ^(UIImage *resultImage, void(^commit)(void)) { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) return; - - if (strongSelf.didFinishWithImage != nil) - strongSelf.didFinishWithImage(resultImage); - - __strong TGMediaAssetsController *strongController = weakController; - if (strongController != nil && strongController.dismissalBlock != nil) - strongController.dismissalBlock(); + + if (strongSelf.willFinishWithImage != nil) { + strongSelf.willFinishWithImage(resultImage, ^{ + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + commit(); + + __strong TGMediaAssetsController *strongController = weakController; + if (strongController != nil && strongController.dismissalBlock != nil) + strongController.dismissalBlock(); + }); + } else { + if (strongSelf.didFinishWithImage != nil) + strongSelf.didFinishWithImage(resultImage); + + commit(); + + __strong TGMediaAssetsController *strongController = weakController; + if (strongController != nil && strongController.dismissalBlock != nil) + strongController.dismissalBlock(); + } }; - controller.avatarVideoCompletionBlock = ^(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments) { + controller.avatarVideoCompletionBlock = ^(UIImage *image, AVAsset *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)) { __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf; if (strongSelf == nil) return; - if (strongSelf.didFinishWithVideo != nil) - strongSelf.didFinishWithVideo(image, asset, adjustments); - __strong TGMediaAssetsController *strongController = weakController; - if (strongController != nil && strongController.dismissalBlock != nil) - strongController.dismissalBlock(); + if (strongSelf.willFinishWithVideo != nil) { + strongSelf.willFinishWithVideo(image, ^{ + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + + __strong TGMediaAssetsController *strongController = weakController; + if (strongController != nil && strongController.dismissalBlock != nil) + strongController.dismissalBlock(); + }); + } else { + if (strongSelf.didFinishWithVideo != nil) + strongSelf.didFinishWithVideo(image, asset, adjustments); + + commit(); + + __strong TGMediaAssetsController *strongController = weakController; + if (strongController != nil && strongController.dismissalBlock != nil) + strongController.dismissalBlock(); + } }; return presentBlock(controller); }; diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 726fe4b36e6..5578835d546 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -54,6 +54,16 @@ + (instancetype)timerUpdate:(NSNumber *)timer; @end +@interface TGMediaSpoilerUpdate : NSObject + +@property (nonatomic, readonly, strong) id item; +@property (nonatomic, readonly) bool spoiler; + ++ (instancetype)spoilerUpdateWithItem:(id)item spoiler:(bool)spoiler; ++ (instancetype)spoilerUpdate:(bool)spoiler; + +@end + @interface TGModernCache (Private) @@ -69,6 +79,8 @@ @interface TGMediaEditingContext () NSMutableDictionary *_adjustments; NSMutableDictionary *_timers; NSNumber *_timer; + + NSMutableDictionary *_spoilers; SQueue *_queue; @@ -99,6 +111,7 @@ @interface TGMediaEditingContext () SPipe *_adjustmentsPipe; SPipe *_captionPipe; SPipe *_timerPipe; + SPipe *_spoilerPipe; SPipe *_fullSizePipe; SPipe *_cropPipe; @@ -119,6 +132,7 @@ - (instancetype)init _captions = [[NSMutableDictionary alloc] init]; _adjustments = [[NSMutableDictionary alloc] init]; _timers = [[NSMutableDictionary alloc] init]; + _spoilers = [[NSMutableDictionary alloc] init]; _imageCache = [[TGMemoryImageCache alloc] initWithSoftMemoryLimit:[[self class] imageSoftMemoryLimit] hardMemoryLimit:[[self class] imageHardMemoryLimit]]; @@ -165,6 +179,7 @@ - (instancetype)init _adjustmentsPipe = [[SPipe alloc] init]; _captionPipe = [[SPipe alloc] init]; _timerPipe = [[SPipe alloc] init]; + _spoilerPipe = [[SPipe alloc] init]; _fullSizePipe = [[SPipe alloc] init]; _cropPipe = [[SPipe alloc] init]; } @@ -596,6 +611,73 @@ - (SSignal *)timersUpdatedSignal #pragma mark - +- (bool)spoilerForItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return nil; + + return [self _spoilerForItemId:itemId]; +} + +- (bool)_spoilerForItemId:(NSString *)itemId +{ + if (itemId == nil) + return nil; + + return _spoilers[itemId]; +} + +- (void)setSpoiler:(bool)spoiler forItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return; + + if (spoiler) + _spoilers[itemId] = @true; + else + [_spoilers removeObjectForKey:itemId]; + + _spoilerPipe.sink([TGMediaSpoilerUpdate spoilerUpdateWithItem:item spoiler:spoiler]); +} + +- (SSignal *)spoilerSignalForItem:(NSObject *)item +{ + SSignal *updateSignal = [[_spoilerPipe.signalProducer() filter:^bool(TGMediaSpoilerUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:item.uniqueIdentifier]; + }] map:^NSNumber *(TGMediaSpoilerUpdate *update) + { + return @(update.spoiler); + }]; + + return [[SSignal single:@([self spoilerForItem:item])] then:updateSignal]; +} + +- (SSignal *)spoilerSignalForIdentifier:(NSString *)identifier +{ + SSignal *updateSignal = [[_spoilerPipe.signalProducer() filter:^bool(TGMediaSpoilerUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:identifier]; + }] map:^NSNumber *(TGMediaSpoilerUpdate *update) + { + return @(update.spoiler); + }]; + + return [[SSignal single:@([self _spoilerForItemId:identifier])] then:updateSignal]; +} + +- (SSignal *)spoilersUpdatedSignal +{ + return [_spoilerPipe.signalProducer() map:^id(__unused id value) + { + return @true; + }]; +} + +#pragma mark - + - (void)setImage:(UIImage *)image thumbnailImage:(UIImage *)thumbnailImage forItem:(id)item synchronous:(bool)synchronous { NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; @@ -633,7 +715,7 @@ - (void)setImage:(UIImage *)image thumbnailImage:(UIImage *)thumbnailImage forIt [_queue dispatch:block]; } -- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video +- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video { NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; @@ -643,6 +725,7 @@ - (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImag NSURL *imagesDirectory = video ? _videoPaintingImagesUrl : _paintingImagesUrl; NSURL *imageUrl = [imagesDirectory URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", [TGStringUtils md5:itemId]]]; NSURL *dataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.dat", [TGStringUtils md5:itemId]]]; + NSURL *entitiesDataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@_entities.dat", [TGStringUtils md5:itemId]]]; [_paintingImageCache setImage:image forKey:itemId attributes:NULL]; @@ -651,12 +734,16 @@ - (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImag bool imageSuccess = [imageData writeToURL:imageUrl options:NSDataWritingAtomic error:nil]; [[NSFileManager defaultManager] removeItemAtURL:dataUrl error:nil]; bool dataSuccess = [data writeToURL:dataUrl options:NSDataWritingAtomic error:nil]; + bool entitiesDataSuccess = [entitiesData writeToURL:entitiesDataUrl options:NSDataWritingAtomic error:nil]; if (imageSuccess && imageOutUrl != NULL) *imageOutUrl = imageUrl; if (dataSuccess && dataOutUrl != NULL) *dataOutUrl = dataUrl; + + if (entitiesDataSuccess && entitiesDataOutUrl != NULL) + *entitiesDataOutUrl = entitiesDataUrl; if (video) [_storeVideoPaintingImages addObject:imageUrl]; @@ -1082,3 +1169,23 @@ + (instancetype)timerUpdate:(NSNumber *)timer } @end + + +@implementation TGMediaSpoilerUpdate + ++ (instancetype)spoilerUpdateWithItem:(id)item spoiler:(bool)spoiler +{ + TGMediaSpoilerUpdate *update = [[TGMediaSpoilerUpdate alloc] init]; + update->_item = item; + update->_spoiler = spoiler; + return update; +} + ++ (instancetype)spoilerUpdate:(bool)spoiler +{ + TGMediaSpoilerUpdate *update = [[TGMediaSpoilerUpdate alloc] init]; + update->_spoiler = spoiler; + return update; +} + +@end diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index dc215d037a9..53ebd775e0a 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -21,8 +21,6 @@ #import -#import "TGPhotoEntitiesContainerView.h" - @interface TGMediaPickerGalleryModel () { TGMediaPickerGalleryInterfaceView *_interfaceView; @@ -363,7 +361,7 @@ - (void)presentPhotoEditorForItem:(id)item tab:(TGP UIView *referenceParentView = nil; UIImage *image = nil; - TGPhotoEntitiesContainerView *entitiesView = nil; + UIView *entitiesView = nil; id editableMediaItem = item.editableMediaItem; @@ -373,7 +371,7 @@ - (void)presentPhotoEditorForItem:(id)item tab:(TGP screenImage = [(UIImageView *)editorReferenceView image]; referenceView = editorReferenceView; - if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject isKindOfClass:[TGPhotoEntitiesContainerView class]]) { + if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject conformsToProtocol:@protocol(TGPhotoDrawingEntitiesView)]) { entitiesView = editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject; } } @@ -417,7 +415,7 @@ - (void)presentPhotoEditorForItem:(id)item tab:(TGP }; void (^didFinishEditingItem)(iditem, id adjustments, UIImage *resultImage, UIImage *thumbnailImage) = self.didFinishEditingItem; - controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) + controller.didFinishEditing = ^(id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { __strong TGMediaPickerGalleryModel *strongSelf = weakSelf; if (strongSelf == nil) { @@ -442,6 +440,8 @@ - (void)presentPhotoEditorForItem:(id)item tab:(TGP [videoItemView setScrubbingPanelApperanceLocked:false]; [videoItemView presentScrubbingPanelAfterReload:hasChanges]; } + + commit(); }; controller.didFinishRenderingFullSizeImage = ^(UIImage *image) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m index aa7bedcda22..0d17c5524ed 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m @@ -20,8 +20,7 @@ #import "TGMediaPickerGalleryPhotoItem.h" -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoPaintController.h" +#import "TGPhotoDrawingController.h" #import @@ -42,7 +41,7 @@ @interface TGMediaPickerGalleryPhotoItemView () UIView *_contentView; UIView *_contentWrapperView; - TGPhotoEntitiesContainerView *_entitiesContainerView; + UIView *_entitiesView; SMetaDisposable *_adjustmentsDisposable; SMetaDisposable *_attributesDisposable; @@ -90,11 +89,7 @@ - (instancetype)initWithFrame:(CGRect)frame _contentWrapperView = [[UIView alloc] init]; [_contentView addSubview:_contentWrapperView]; - - _entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init]; - _entitiesContainerView.userInteractionEnabled = false; - [_contentWrapperView addSubview:_entitiesContainerView]; - + _fileInfoLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 20)]; _fileInfoLabel.backgroundColor = [UIColor clearColor]; _fileInfoLabel.font = TGSystemFontOfSize(13); @@ -143,7 +138,11 @@ - (void)setItem:(TGMediaPickerGalleryPhotoItem *)item synchronously:(bool)synchr [super setItem:item synchronously:synchronously]; - _entitiesContainerView.stickersContext = item.stickersContext; + if (_entitiesView == nil) { + _entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.asset.originalSize]; + _entitiesView.userInteractionEnabled = false; + [_contentWrapperView addSubview:_entitiesView]; + } _imageSize = item.asset.originalSize; [self reset]; @@ -223,7 +222,7 @@ - (void)setItem:(TGMediaPickerGalleryPhotoItem *)item synchronously:(bool)synchr return; [strongSelf layoutEntities]; - [strongSelf->_entitiesContainerView setupWithPaintingData:next.paintingData]; + [strongSelf->_entitiesView setupWithEntitiesData:next.paintingData.entitiesData]; }]]; } @@ -486,27 +485,27 @@ - (void)_layoutPlayerViewWithCropRect:(CGRect)cropRect orientation:(UIImageOrien _contentView.transform = rotationTransform; _contentView.frame = previewFrame; - CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize]; - CGRect fittedCropRect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false]; + CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize]; + CGRect fittedCropRect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false]; _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height); CGFloat contentScale = _contentView.bounds.size.width / fittedCropRect.size.width; _contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale); _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height); - CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true]; - _entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); - _entitiesContainerView.transform = CGAffineTransformMakeRotation(rotation); + CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true]; + _entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); + _entitiesView.transform = CGAffineTransformMakeRotation(rotation); - CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]); CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation); CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); CGFloat scale = fittedOriginalSize.width / originalSize.width; - CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoPaintController fittedCropRect:cropRect centerScale:scale]); + CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoDrawingController fittedCropRect:cropRect centerScale:scale]); CGPoint boundsCenter = TGPaintCenterOfRect(_contentWrapperView.bounds); - _entitiesContainerView.center = TGPaintAddPoints(boundsCenter, offset); + _entitiesView.center = TGPaintAddPoints(boundsCenter, offset); } @end diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 9d4aa9a9d0b..19a85c0db09 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -32,8 +32,7 @@ #import "TGMediaPickerScrubberHeaderView.h" #import "TGPhotoEditorPreviewView.h" -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoPaintController.h" +#import "TGPhotoDrawingController.h" #import #import "TGModernGalleryVideoContentView.h" @@ -84,7 +83,7 @@ @interface TGMediaPickerGalleryVideoItemView() *_entitiesView; NSTimer *_positionTimer; TGObserverProxy *_didPlayToEndObserver; @@ -174,12 +173,7 @@ - (instancetype)initWithFrame:(CGRect)frame _contentWrapperView = [[UIView alloc] init]; [_contentView addSubview:_contentWrapperView]; - - _entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init]; - _entitiesContainerView.hidden = true; - _entitiesContainerView.userInteractionEnabled = false; - [_contentWrapperView addSubview:_entitiesContainerView]; - + _curtainView = [[UIView alloc] init]; _curtainView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _curtainView.backgroundColor = [UIColor blackColor]; @@ -425,8 +419,14 @@ - (void)setItem:(TGMediaPickerGalleryVideoItem *)item synchronously:(bool)synchr _scrubberView.allowsTrimming = false; _videoDimensions = item.dimensions; - _entitiesContainerView.stickersContext = item.stickersContext; + if (_entitiesView == nil) { + _entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.dimensions]; + _entitiesView.hidden = true; + _entitiesView.userInteractionEnabled = false; + [_contentWrapperView addSubview:_entitiesView]; + } + __weak TGMediaPickerGalleryVideoItemView *weakSelf = self; [_videoDurationVar set:[[[item.durationSignal deliverOn:[SQueue mainQueue]] catch:^SSignal *(__unused id error) { @@ -504,8 +504,8 @@ - (void)setItem:(TGMediaPickerGalleryVideoItem *)item synchronously:(bool)synchr if (baseAdjustments.sendAsGif || ([strongSelf itemIsLivePhoto])) [strongSelf setPlayButtonHidden:true animated:false]; - [strongSelf->_entitiesContainerView setupWithPaintingData:adjustments.paintingData]; - [strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying]; + [strongSelf->_entitiesView setupWithEntitiesData:adjustments.paintingData.entitiesData]; + [strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying]; [strongSelf->_photoEditor importAdjustments:adjustments]; if (!strongSelf.isPlaying) { @@ -823,24 +823,24 @@ - (void)_layoutPlayerViewWithCropRect:(CGRect)cropRect videoFrameSize:(CGSize)vi _contentView.transform = rotationTransform; _contentView.frame = _imageView.frame; - CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize]; + CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize]; _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height); CGFloat contentScale = ratio; _contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale); _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height); - CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true]; - _entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); - _entitiesContainerView.transform = CGAffineTransformMakeRotation(0.0); + CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true]; + _entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); + _entitiesView.transform = CGAffineTransformMakeRotation(0.0); - CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]); CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, 0.0); __unused CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); } -- (TGPhotoEntitiesContainerView *)entitiesView { - return _entitiesContainerView; +- (UIView *)entitiesView { + return _entitiesView; } - (void)singleTap @@ -1176,7 +1176,7 @@ - (void)preparePlayerAndPlay:(bool)play strongSelf->_videoView.userInteractionEnabled = false; [strongSelf->_playerContainerView insertSubview:strongSelf->_videoView belowSubview:strongSelf->_paintingImageView]; - strongSelf->_entitiesContainerView.hidden = false; + strongSelf->_entitiesView.hidden = false; [strongSelf->_videoView setNeedsTransitionIn]; [strongSelf->_videoView performTransitionInIfNeeded]; @@ -1196,7 +1196,7 @@ - (void)preparePlayerAndPlay:(bool)play [strongSelf->_player play]; } - [strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying]; + [strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying]; strongSelf->_positionTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(positionTimerEvent) interval:0.25 repeat:true]; [strongSelf positionTimerEvent]; @@ -1211,11 +1211,11 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N { if (_player.rate > FLT_EPSILON) { [_scrubberView setIsPlaying:true]; - [_entitiesContainerView updateVisibility:true]; + [_entitiesView updateVisibility:true]; } else { [_scrubberView setIsPlaying:false]; - [_entitiesContainerView updateVisibility:false]; + [_entitiesView updateVisibility:false]; } } } @@ -1251,7 +1251,7 @@ - (void)play [self positionTimerEvent]; } - [_entitiesContainerView updateVisibility:true]; + [_entitiesView updateVisibility:true]; } - (void)playIfAvailable @@ -1274,7 +1274,7 @@ - (void)stop [_positionTimer invalidate]; _positionTimer = nil; - [_entitiesContainerView updateVisibility:false]; + [_entitiesView updateVisibility:false]; } - (void)togglePlayback diff --git a/submodules/LegacyComponents/Sources/TGMenuSheetTitleItemView.m b/submodules/LegacyComponents/Sources/TGMenuSheetTitleItemView.m index 6eea101534b..15ae64ee8ae 100644 --- a/submodules/LegacyComponents/Sources/TGMenuSheetTitleItemView.m +++ b/submodules/LegacyComponents/Sources/TGMenuSheetTitleItemView.m @@ -73,7 +73,7 @@ - (void)setPallete:(TGMenuSheetPallete *)pallete - (CGFloat)preferredHeightForWidth:(CGFloat)width screenHeight:(CGFloat)__unused screenHeight { - CGFloat height = 17.0f; + CGFloat height = 16.0f; if (_titleLabel.text.length > 0) { @@ -91,19 +91,19 @@ - (CGFloat)preferredHeightForWidth:(CGFloat)width screenHeight:(CGFloat)__unused height += _subtitleLabel.frame.size.height; } - height += 15.0f; + height += 8.0f; return height; } - (bool)requiresDivider { - return true; + return false; } - (void)layoutSubviews { - CGFloat topOffset = 17.0f; + CGFloat topOffset = 16.0f; if (_titleLabel.text.length > 0) { diff --git a/submodules/LegacyComponents/Sources/TGPaintArrowBrush.h b/submodules/LegacyComponents/Sources/TGPaintArrowBrush.h deleted file mode 100644 index 36b1c0e5639..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintArrowBrush.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPaintBrush.h" - -@interface TGPaintArrowBrush : TGPaintBrush - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintArrowBrush.m b/submodules/LegacyComponents/Sources/TGPaintArrowBrush.m deleted file mode 100644 index cc366389839..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintArrowBrush.m +++ /dev/null @@ -1,90 +0,0 @@ -#import "TGPaintArrowBrush.h" - -const CGFloat TGPaintArrowBrushHardness = 0.92f; - -@implementation TGPaintArrowBrush - -- (CGFloat)spacing -{ - return 0.15f; -} - -- (CGFloat)alpha -{ - return 0.85f; -} - -- (CGFloat)angle -{ - return 0.0f; -} - -//- (CGFloat)dynamic -//{ -// return 0.75f; -//} - -- (bool)arrow -{ - return true; -} - -- (CGImageRef)generateRadialStampForSize:(CGSize)size hardness:(CGFloat)hardness -{ - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray(); - CGContextRef ctx = CGBitmapContextCreate(NULL, (NSInteger)size.width, (NSInteger)size.height, 8, (NSInteger)size.width, colorspace, kCGImageAlphaNone); - - CGContextSetGrayFillColor(ctx, 0.0f, 1.0f); - CGContextFillRect(ctx, CGRectMake(0, 0, size.width, size.height)); - - NSArray *colors = @[(__bridge id) [UIColor whiteColor].CGColor, (__bridge id) [UIColor blackColor].CGColor]; - const CGFloat locations[] = {0.0, 1.0}; - - CGGradientRef gradientRef = CGGradientCreateWithColors(colorspace, (__bridge CFArrayRef) colors, locations); - CGPoint center = CGPointMake(size.width / 2, size.height / 2); - - CGFloat maxRadius = size.width / 2; - CGFloat hFactor = hardness * 0.99; - CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation |kCGGradientDrawsAfterEndLocation; - CGContextDrawRadialGradient(ctx, gradientRef, center, hFactor * maxRadius, center, maxRadius, options); - - CGImageRef image = CGBitmapContextCreateImage(ctx); - - CGContextRelease(ctx); - CGColorSpaceRelease(colorspace); - CGGradientRelease(gradientRef); - - return image; -} - -- (CGImageRef)stampRef -{ - static CGImageRef image = NULL; - - if (image == NULL) - image = [self generateRadialStampForSize:TGPaintBrushTextureSize hardness:TGPaintArrowBrushHardness]; - - return image; -} - -- (CGImageRef)previewStampRef -{ - if (_previewStampRef == NULL) - _previewStampRef = [self generateRadialStampForSize:TGPaintBrushPreviewTextureSize hardness:TGPaintArrowBrushHardness]; - - return _previewStampRef; -} - -static UIImage *radialBrushPreviewImage = nil; - -- (UIImage *)previewImage -{ - return radialBrushPreviewImage; -} - -- (void)setPreviewImage:(UIImage *)previewImage -{ - radialBrushPreviewImage = previewImage; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintBrush.h b/submodules/LegacyComponents/Sources/TGPaintBrush.h deleted file mode 100644 index 84c6987a727..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBrush.h +++ /dev/null @@ -1,25 +0,0 @@ -#import -#import - -@interface TGPaintBrush : NSObject -{ - CGImageRef _previewStampRef; -} - -@property (nonatomic, readonly) CGFloat spacing; -@property (nonatomic, readonly) CGFloat alpha; -@property (nonatomic, readonly) CGFloat angle; -@property (nonatomic, readonly) CGFloat scale; -@property (nonatomic, readonly) CGFloat dynamic; -@property (nonatomic, readonly) bool lightSaber; -@property (nonatomic, readonly) bool arrow; - -@property (nonatomic, readonly) CGImageRef stampRef; -@property (nonatomic, readonly) CGImageRef previewStampRef; - -@property (nonatomic, strong) UIImage *previewImage; - -@end - -extern const CGSize TGPaintBrushTextureSize; -extern const CGSize TGPaintBrushPreviewTextureSize; diff --git a/submodules/LegacyComponents/Sources/TGPaintBrush.m b/submodules/LegacyComponents/Sources/TGPaintBrush.m deleted file mode 100644 index 6ae5cad0ba3..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBrush.m +++ /dev/null @@ -1,91 +0,0 @@ -#import "TGPaintBrush.h" - -#import - -const CGSize TGPaintBrushTextureSize = { 384.0f, 384.0f }; -const CGSize TGPaintBrushPreviewTextureSize = { 64.0f, 64.0f }; - -@interface TGPaintBrush () -{ - NSInteger _uuid; -} -@end - -@implementation TGPaintBrush - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - arc4random_buf(&_uuid, sizeof(NSInteger)); - } - return self; -} - -- (void)dealloc -{ - if (_previewStampRef != NULL) - CGImageRelease(_previewStampRef); -} - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPaintBrush *brush = (TGPaintBrush *)object; - return (_uuid == brush->_uuid); -} - -- (CGFloat)spacing -{ - return 1.0f; -} - -- (CGFloat)alpha -{ - return 1.0f; -} - -- (CGFloat)angle -{ - return 0.0f; -} - -- (CGFloat)scale -{ - return 1.0f; -} - -- (CGFloat)dynamic -{ - return 0.0f; -} - -- (bool)lightSaber -{ - return false; -} - -- (CGImageRef)stampRef -{ - return NULL; -} - -- (CGImageRef)previewStampRef -{ - if (_previewStampRef == NULL) - { - UIImage *image = TGScaleImageToPixelSize([UIImage imageWithCGImage:self.stampRef], TGPaintBrushPreviewTextureSize); - _previewStampRef = image.CGImage; - CGImageRetain(_previewStampRef); - } - - return _previewStampRef; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintBrushPreview.h b/submodules/LegacyComponents/Sources/TGPaintBrushPreview.h deleted file mode 100644 index 533cf1ce10d..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBrushPreview.h +++ /dev/null @@ -1,11 +0,0 @@ -#import -#import - -@class TGPainting; -@class TGPaintBrush; - -@interface TGPaintBrushPreview : NSObject - -- (UIImage *)imageForBrush:(TGPaintBrush *)brush size:(CGSize)size; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintBrushPreview.m b/submodules/LegacyComponents/Sources/TGPaintBrushPreview.m deleted file mode 100644 index ed39c117905..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBrushPreview.m +++ /dev/null @@ -1,366 +0,0 @@ -#import "TGPaintBrushPreview.h" - -#import - -#import - -#import "matrix.h" - -#import "TGPainting.h" -#import "TGPaintBrush.h" -#import "TGPaintPath.h" -#import "TGPaintRender.h" -#import "TGPaintShader.h" -#import "TGPaintShaderSet.h" -#import "TGPaintTexture.h" -#import - -const NSUInteger TGPaintBrushPreviewSegmentsCount = 100; - -@interface TGPaintBrushPreview () -{ - EAGLContext *_context; - - TGPaintBrush *_brush; - TGPaintShader *_brushShader; - TGPaintShader *_brushLightShader; - TGPaintShader *_blitLightShader; - TGPaintTexture *_brushTexture; - TGPaintRenderState *_renderState; - - GLuint _quadVAO; - GLuint _quadVBO; - - TGPaintPath *_path; - - GLubyte *_data; - CGContextRef _cgContext; - - GLuint _framebuffer; - GLuint _maskTextureName; - - GLuint _lightFramebuffer; - GLuint _lightTextureName; - - GLfloat _projection[16]; - GLint _width; - GLint _height; - - GLenum _format; - GLenum _type; -} -@end - -@implementation TGPaintBrushPreview - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; - - _format = GL_RGBA; - _type = GL_UNSIGNED_BYTE; - - if (_context == nil || ![EAGLContext setCurrentContext:_context]) - return nil; - - glEnable(GL_BLEND); - glDisable(GL_DITHER); - glDisable(GL_STENCIL_TEST); - glDisable(GL_DEPTH_TEST); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glGenFramebuffers(1, &_framebuffer); - glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); - - glGenFramebuffers(1, &_lightFramebuffer); - - NSDictionary *availableShaders = [TGPaintShaderSet availableShaders]; - - NSDictionary *shader = availableShaders[@"brush"]; - _brushShader = [[TGPaintShader alloc] initWithVertexShader:shader[@"vertex"] fragmentShader:shader[@"fragment"] attributes:shader[@"attributes"] uniforms:shader[@"uniforms"]]; - - shader = availableShaders[@"brushLight"]; - _brushLightShader = [[TGPaintShader alloc] initWithVertexShader:shader[@"vertex"] fragmentShader:shader[@"fragment"] attributes:shader[@"attributes"] uniforms:shader[@"uniforms"]]; - - shader = availableShaders[@"brushLightPreview"]; - _blitLightShader = [[TGPaintShader alloc] initWithVertexShader:shader[@"vertex"] fragmentShader:shader[@"fragment"] attributes:shader[@"attributes"] uniforms:shader[@"uniforms"]]; - - _renderState = [[TGPaintRenderState alloc] init]; - - TGPaintHasGLError(); - } - return self; -} - -- (void)dealloc -{ - if (_context == nil) - return; - - [EAGLContext setCurrentContext:_context]; - - if (_cgContext != NULL) - CGContextRelease(_cgContext); - - _brush = nil; - - glDeleteBuffers(1, &_quadVBO); - glDeleteVertexArraysOES(1, &_quadVAO); - - if (_framebuffer != 0) - { - glDeleteFramebuffers(1, &_framebuffer); - _framebuffer = 0; - } - - if (_maskTextureName != 0) - { - glDeleteTextures(1, &_maskTextureName); - _maskTextureName = 0; - } - - if (_lightFramebuffer != 0) - { - glDeleteFramebuffers(1, &_lightFramebuffer); - _lightFramebuffer = 0; - } - - if (_lightTextureName != 0) - { - glDeleteTextures(1, &_lightTextureName); - _lightTextureName = 0; - } - - [EAGLContext setCurrentContext:nil]; -} - -- (void)setSize:(CGSize)size -{ - if (_width == size.width && _height == size.height) - return; - - _width = (GLint)size.width; - _height = (GLint)size.height; - - if (_data != NULL) - free(_data); - - _data = malloc(_width * _height * 4); - - if (_cgContext != NULL) - CGContextRelease(_cgContext); - - CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - _cgContext = CGBitmapContextCreate(_data, _width, _height, 8, _width * 4, colorSpaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast); - - CGColorSpaceRelease(colorSpaceRef); - - if (_path != nil) - _path = nil; - - [self _generateBrushPath]; - - GLfloat mProj[16], mScale[16]; - mat4f_LoadOrtho(0, _width, 0, _height, -1.0f, 1.0f, mProj); - - CGFloat scale = MIN(2.0f, TGScreenScaling()); - CGAffineTransform tX = CGAffineTransformMakeScale(scale, scale); - mat4f_LoadCGAffineTransform(mScale, tX); - - mat4f_MultiplyMat4f(mProj, mScale, _projection); - - glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); - - glGenTextures(1, &_maskTextureName); - glBindTexture(GL_TEXTURE_2D, _maskTextureName); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - glBindTexture(GL_TEXTURE_2D, _maskTextureName); - glTexImage2D(GL_TEXTURE_2D, 0, _format, _width, _height, 0, _format, _type, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _maskTextureName, 0); - - - glBindFramebuffer(GL_FRAMEBUFFER, _lightFramebuffer); - - glGenTextures(1, &_lightTextureName); - glBindTexture(GL_TEXTURE_2D, _lightTextureName); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - glBindTexture(GL_TEXTURE_2D, _lightTextureName); - glTexImage2D(GL_TEXTURE_2D, 0, _format, _width, _height, 0, _format, _type, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _lightTextureName, 0); - - TGPaintHasGLError(); -} - -- (void)_generateBrushPath -{ - if (_path != nil) - return; - - CGFloat scale = MIN(2.0f, TGScreenScaling()); - - CGPoint start = CGPointMake(15.0f, _height / (2.0f * scale)); - CGFloat width = (_width / scale) - 2.0f * 15.0f; - CGFloat amplitude = 6.0f; - - NSMutableArray *points = [[NSMutableArray alloc] init]; - for (NSUInteger i = 0; i < TGPaintBrushPreviewSegmentsCount; i++) - { - CGFloat fraction = (CGFloat)i / (TGPaintBrushPreviewSegmentsCount - 1); - CGPoint pt = CGPointMake(start.x + width * fraction, start.y + sin(-fraction * 2 * M_PI) * amplitude); - - TGPaintPoint *point = [TGPaintPoint pointWithX:pt.x y:pt.y z:fraction]; - [points addObject:point]; - - if (i == 0 || i == TGPaintBrushPreviewSegmentsCount - 1) - point.edge = true; - } - - _path = [[TGPaintPath alloc] initWithPoints:points]; - _path.baseWeight = 12.0f; - _path.color = [UIColor redColor]; -} - -- (void)_setupBrush -{ - TGPaintShader *shader = _brush.lightSaber ? _brushLightShader : _brushShader; - glUseProgram(shader.program); - glActiveTexture(GL_TEXTURE0); - - if (_brushTexture == nil) - _brushTexture = [[TGPaintTexture alloc] initWithCGImage:_brush.previewStampRef forceRGB:false]; - - glBindTexture(GL_TEXTURE_2D, _brushTexture.textureName); - - glUniform1i([shader uniformForKey:@"texture"], 0); - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, _projection); -} - -- (void)_cleanBrushResources -{ - if (_brushTexture == nil) - return; - - [EAGLContext setCurrentContext:_context]; - [_brushTexture cleanResources]; - _brushTexture = nil; -} - -- (UIImage *)imageForBrush:(TGPaintBrush *)brush size:(CGSize)size -{ - if (![brush isEqual:_brush]) - [self _cleanBrushResources]; - - _brush = brush; - - CGFloat scale = MIN(2.0f, TGScreenScaling()); - size = TGPaintMultiplySizeScalar(size, scale); - - [EAGLContext setCurrentContext:_context]; - [self setSize:size]; - - glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); - glViewport(0, 0, _width, _height); - - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - - TGPaintHasGLError(); - - [self _setupBrush]; - [_renderState reset]; - _path.remainder = 0.0f; - _path.pressureRemainder = 0.0f; - _path.brush = brush; - - [TGPaintRender renderPath:_path renderState:_renderState]; - - if (_brush.lightSaber) - { - glBindFramebuffer(GL_FRAMEBUFFER, _lightFramebuffer); - glViewport(0, 0, _width, _height); - - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - - TGPaintShader *shader = _blitLightShader; - glUseProgram(shader.program); - - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, _projection); - glUniform1i([shader uniformForKey:@"mask"], 0); - TGSetupColorUniform([shader uniformForKey:@"color"], [UIColor blackColor]); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, _maskTextureName); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindVertexArrayOES(0); - } - - glReadPixels(0, 0, _width, _height, GL_RGBA, GL_UNSIGNED_BYTE, _data); - CGImageRef imageRef = CGBitmapContextCreateImage(_cgContext); - UIImage *result = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - CGImageRelease(imageRef); - - return result; -} - -- (GLuint)_quad -{ - if (_quadVAO == 0) - { - [EAGLContext setCurrentContext:_context]; - CGFloat scale = MIN(2.0f, TGScreenScaling()); - CGRect rect = CGRectMake(0, 0, _width / scale, _height / scale); - - CGPoint corners[4]; - corners[0] = rect.origin; - corners[1] = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)); - corners[2] = CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)); - corners[3] = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)); - - const GLfloat vertices[] = - { - (GLfloat)corners[0].x, (GLfloat)corners[0].y, 0.0, 0.0, - (GLfloat)corners[1].x, (GLfloat)corners[1].y, 1.0, 0.0, - (GLfloat)corners[3].x, (GLfloat)corners[3].y, 0.0, 1.0, - (GLfloat)corners[2].x, (GLfloat)corners[2].y, 1.0, 1.0, - }; - - glGenVertexArraysOES(1, &_quadVAO); - glBindVertexArrayOES(_quadVAO); - - glGenBuffers(1, &_quadVBO); - glBindBuffer(GL_ARRAY_BUFFER, _quadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 16, vertices, GL_STATIC_DRAW); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void*)8); - glEnableVertexAttribArray(1); - - glBindBuffer(GL_ARRAY_BUFFER,0); - glBindVertexArrayOES(0); - } - - return _quadVAO; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintBuffers.h b/submodules/LegacyComponents/Sources/TGPaintBuffers.h deleted file mode 100644 index 6ff06bd88f4..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBuffers.h +++ /dev/null @@ -1,20 +0,0 @@ -#import -#import -#import - -@interface TGPaintBuffers : NSObject - -@property (nonatomic, weak) EAGLContext *context; -@property (nonatomic, readonly) CAEAGLLayer *layer; -@property (nonatomic, readonly) GLuint renderbuffer; -@property (nonatomic, readonly) GLuint framebuffer; -@property (nonatomic, readonly) GLuint stencilBuffer; -@property (nonatomic, readonly) GLint width; -@property (nonatomic, readonly) GLint height; - -- (bool)update; -- (void)present; - -+ (instancetype)buffersWithGLContext:(EAGLContext *)context layer:(CAEAGLLayer *)layer; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintBuffers.m b/submodules/LegacyComponents/Sources/TGPaintBuffers.m deleted file mode 100644 index 087367928d0..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintBuffers.m +++ /dev/null @@ -1,102 +0,0 @@ -#import "TGPaintBuffers.h" -#import - -#import "LegacyComponentsInternal.h" - -@implementation TGPaintBuffers - -+ (instancetype)buffersWithGLContext:(EAGLContext *)context layer:(CAEAGLLayer *)layer -{ - TGPaintBuffers *c = [[TGPaintBuffers alloc] init]; - - c->_layer = layer; - layer.opaque = false; - layer.drawableProperties = @ - { - kEAGLDrawablePropertyRetainedBacking: @true, - kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 - }; - - c.context = context; - [EAGLContext setCurrentContext:context]; - - glGenFramebuffers(1, &c->_framebuffer); - glGenRenderbuffers(1, &c->_renderbuffer); - glBindFramebuffer(GL_FRAMEBUFFER, c->_framebuffer); - glBindRenderbuffer(GL_RENDERBUFFER, c->_renderbuffer); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, c->_renderbuffer); - - glGenRenderbuffers(1, &c->_stencilBuffer); - glBindRenderbuffer(GL_RENDERBUFFER, c->_stencilBuffer); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, c->_stencilBuffer); - - TGPaintHasGLError(); - - return c; -} - -- (void)dealloc -{ - [self cleanResources]; -} - -- (void)cleanResources -{ - [EAGLContext setCurrentContext:_context]; - - if (_framebuffer != 0) - { - glDeleteFramebuffers(1, &_framebuffer); - _framebuffer = 0; - } - - if (_renderbuffer != 0) - { - glDeleteRenderbuffers(1, &_renderbuffer); - _renderbuffer = 0; - } - - if (_stencilBuffer) - { - glDeleteBuffers(1, &_stencilBuffer); - _stencilBuffer = 0; - } - - TGPaintHasGLError(); -} - -- (bool)update -{ - [EAGLContext setCurrentContext:_context]; - TGPaintHasGLError(); - - glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); - glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer); - - [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_layer]; - - glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_width); - glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_height); - - glBindRenderbuffer(GL_RENDERBUFFER, _stencilBuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, _width, _height); - - TGPaintHasGLError(); - - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - { - TGLegacyLog(@"Failed to create complete framebuffer %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); - return false; - } - - return true; -} - -- (void)present -{ - glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer); - glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer); - [_context presentRenderbuffer:GL_RENDERBUFFER]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintCanvas.h b/submodules/LegacyComponents/Sources/TGPaintCanvas.h deleted file mode 100644 index 11bd547e0a2..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintCanvas.h +++ /dev/null @@ -1,33 +0,0 @@ -#import - -@class TGPainting; -@class TGPaintBrush; -@class TGPaintState; - -@interface TGPaintCanvas : UIView - -@property (nonatomic, strong) TGPainting *painting; -@property (nonatomic, readonly) TGPaintState *state; - -@property (nonatomic, assign) CGRect cropRect; -@property (nonatomic, assign) UIImageOrientation cropOrientation; -@property (nonatomic, assign) CGSize originalSize; - -@property (nonatomic, copy) bool (^shouldDrawOnSingleTap)(void); - -@property (nonatomic, copy) bool (^shouldDraw)(void); -@property (nonatomic, copy) void (^strokeBegan)(void); -@property (nonatomic, copy) void (^strokeCommited)(void); -@property (nonatomic, copy) UIView *(^hitTest)(CGPoint point, UIEvent *event); -@property (nonatomic, copy) bool (^pointInsideContainer)(CGPoint point); - -@property (nonatomic, readonly) bool isTracking; - -- (void)draw; - -- (void)setBrush:(TGPaintBrush *)brush; -- (void)setBrushWeight:(CGFloat)brushWeight; -- (void)setBrushColor:(UIColor *)color; -- (void)setEraser:(bool)eraser; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintCanvas.m b/submodules/LegacyComponents/Sources/TGPaintCanvas.m deleted file mode 100644 index c750b4208e7..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintCanvas.m +++ /dev/null @@ -1,335 +0,0 @@ -#import "TGPaintCanvas.h" - -#import -#import -#import -#import "matrix.h" - -#import "TGPainting.h" -#import "TGPaintBuffers.h" -#import "TGPaintInput.h" -#import "TGPaintState.h" -#import "TGPaintShader.h" -#import -#import - -#import "TGPaintPanGestureRecognizer.h" - -@interface TGPaintCanvas () -{ - TGPaintBuffers *_buffers; - CGFloat _screenScale; - CGAffineTransform _canvasTransform; - CGRect _dirtyRect; - - CGRect _visibleRect; - - TGPaintInput *_input; - TGPaintPanGestureRecognizer *_gestureRecognizer; - bool _beganDrawing; - - __weak dispatch_cancelable_block_t _redrawBlock; -} -@end - -@implementation TGPaintCanvas - -- (instancetype)initWithFrame:(CGRect)frame -{ - _screenScale = MIN(2.0f, [UIScreen mainScreen].scale); - - self = [super initWithFrame:frame]; - if (self != nil) - { - self.contentScaleFactor = _screenScale; - self.multipleTouchEnabled = true; - self.exclusiveTouch = true; - - _state = [[TGPaintState alloc] init]; - - [self _setupGestureRecognizers]; - } - return self; -} - -#pragma mark - Painting - -- (void)setPainting:(TGPainting *)painting -{ - _painting = painting; - - __weak TGPaintCanvas *weakSelf = self; - painting.contentChanged = ^(CGRect rect) - { - __strong TGPaintCanvas *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if (!CGRectEqualToRect(rect, CGRectZero)) - strongSelf->_dirtyRect = TGPaintUnionRect(strongSelf->_dirtyRect, rect); - else - strongSelf->_dirtyRect = [strongSelf visibleRect]; - - [strongSelf _scheduleRedraw]; - }; - painting.strokeCommited = ^ - { - __strong TGPaintCanvas *strongSelf = weakSelf; - if (strongSelf != nil && strongSelf.strokeCommited != nil) - strongSelf.strokeCommited(); - }; - - [self setContext:painting.context]; - [self _updateTransform]; -} - -- (void)setFrame:(CGRect)frame -{ - [super setFrame:frame]; - _visibleRect = self.bounds; - - [self _updateTransform]; -} - -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - - _visibleRect = bounds; -} - -- (void)_updateTransform -{ - CGAffineTransform transform = CGAffineTransformIdentity; - - CGPoint center = TGPaintCenterOfRect(self.bounds); - CGFloat scale = _painting ? self.bounds.size.width / _painting.size.width : 1.0f; - if (scale < FLT_EPSILON) - scale = 1.0f; - - transform = CGAffineTransformTranslate(transform, center.x, center.y); - transform = CGAffineTransformScale(transform, scale, -scale); - transform = CGAffineTransformTranslate(transform, -self.painting.size.width / 2, -self.painting.size.height / 2); - - _canvasTransform = transform; - _input.transform = transform; -} - -#pragma mark - Gesture - -- (void)_setupGestureRecognizers -{ - _input = [[TGPaintInput alloc] init]; - - _gestureRecognizer = [[TGPaintPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _gestureRecognizer.delegate = self; - _gestureRecognizer.minimumNumberOfTouches = 1; - _gestureRecognizer.maximumNumberOfTouches = 2; - - __weak TGPaintCanvas *weakSelf = self; - _gestureRecognizer.shouldRecognizeTap = ^bool - { - __strong TGPaintCanvas *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - if (strongSelf.shouldDrawOnSingleTap != nil) - { - bool drawOnTap = strongSelf.shouldDrawOnSingleTap(); - bool draw = strongSelf.shouldDraw(); - - return draw && drawOnTap; - } - - return false; - }; - [self addGestureRecognizer:_gestureRecognizer]; -} - -- (void)handlePan:(TGPaintPanGestureRecognizer *)gestureRecognizer -{ - if (gestureRecognizer.state == UIGestureRecognizerStateBegan || gestureRecognizer.state == UIGestureRecognizerStateChanged) - { - if (!_beganDrawing) - { - [_input gestureBegan:gestureRecognizer]; - if (self.strokeBegan != nil) - self.strokeBegan(); - _beganDrawing = true; - } - else - { - [_input gestureMoved:gestureRecognizer]; - } - } - else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) - { - [_input gestureEnded:gestureRecognizer]; - _beganDrawing = false; - } - else if (gestureRecognizer.state == UIGestureRecognizerStateCancelled) - { - [_input gestureCanceled:gestureRecognizer]; - _beganDrawing = false; - } -} - -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer -{ - if (self.shouldDraw != nil) - return self.shouldDraw(); - - return true; -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer -{ -// if (gestureRecognizer == _gestureRecognizer && ([otherGestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[UIRotationGestureRecognizer class]])) -// return false; -// - return true; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - UIView *view = [super hitTest:point withEvent:event]; - - if (self.hitTest != nil) - { - UIView *maybeHitView = self.hitTest(point, event); - if (maybeHitView != nil) - view = maybeHitView; - } - - return view; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)__unused event -{ - return self.pointInsideContainer(point); -} - -- (bool)isTracking -{ - return (_gestureRecognizer.state == UIGestureRecognizerStateBegan || _gestureRecognizer.state == UIGestureRecognizerStateChanged); -} - -#pragma mark - Draw - -- (void)draw -{ - [self drawInRect:[self visibleRect]]; -} - -- (void)drawInRect:(CGRect)__unused rect -{ - [EAGLContext setCurrentContext:_buffers.context]; - - glBindFramebuffer(GL_FRAMEBUFFER, _buffers.framebuffer); - glViewport(0, 0, _buffers.width, _buffers.height); - - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - - GLfloat proj[16], effectiveProj[16], final[16]; - mat4f_LoadOrtho(0, (GLfloat)(_buffers.width / _screenScale), 0, (GLfloat)(_buffers.height / _screenScale), -1.0f, 1.0f, proj); - mat4f_LoadCGAffineTransform(effectiveProj, _canvasTransform); - mat4f_MultiplyMat4f(proj, effectiveProj, final); - - [_painting renderWithProjection:final]; - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - [_buffers present]; - - TGPaintHasGLError(); - - _dirtyRect = CGRectZero; -} - -- (void)redrawIfNeeded -{ - if (CGRectEqualToRect(_dirtyRect, CGRectZero)) - return; - - [self drawInRect:_dirtyRect]; -} - -- (void)_scheduleRedraw -{ - if (_redrawBlock != nil) - { - cancel_block(_redrawBlock); - _redrawBlock = nil; - } - - __weak TGPaintCanvas *weakSelf = self; - _redrawBlock = dispatch_after_delay(0.0, [_painting _queue], ^ - { - __strong TGPaintCanvas *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf redrawIfNeeded]; - }); -} - -#pragma mark - - -- (CGRect)visibleRect -{ - return _visibleRect; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - [self.painting performSynchronouslyInContext:^{ - [_buffers update]; - - [self draw]; - }]; -} - -#pragma mark - GL Setup - -- (void)setContext:(EAGLContext *)context -{ - if (context == _buffers.context) - return; - - if (context != nil) - { - _buffers = [TGPaintBuffers buffersWithGLContext:context layer:(CAEAGLLayer *)self.layer]; - [_buffers update]; - } -} - -+ (Class)layerClass -{ - return [CAEAGLLayer class]; -} - -#pragma mark - - -- (void)setBrush:(TGPaintBrush *)brush -{ - _state.brush = brush; - [_painting setBrush:brush]; -} - -- (void)setBrushWeight:(CGFloat)brushWeight -{ - _state.weight = brushWeight; -} - -- (void)setBrushColor:(UIColor *)color -{ - _state.color = color; -} - -- (void)setEraser:(bool)eraser -{ - _state.eraser = eraser; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.h b/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.h deleted file mode 100644 index 97dcb23157b..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPaintBrush.h" - -@interface TGPaintEllipticalBrush : TGPaintBrush - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.m b/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.m deleted file mode 100644 index 6fada1d626e..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintEllipticalBrush.m +++ /dev/null @@ -1,94 +0,0 @@ -#import "TGPaintEllipticalBrush.h" -#import - -const CGFloat TGPaintEllipticalBrushHardness = 0.89f; -const CGFloat TGPaintEllipticalBrushAngle = 110.0f; -const CGFloat TGPaintEllipticalBrushRoundness = 0.35f; - -@implementation TGPaintEllipticalBrush - -- (CGFloat)spacing -{ - return 0.075f; -} - -- (CGFloat)alpha -{ - return 0.17f; -} - -- (CGFloat)angle -{ - return TGDegreesToRadians(TGPaintEllipticalBrushAngle); -} - -- (CGFloat)scale -{ - return 1.5f; -} - -- (CGImageRef)generateEllipticalStampForSize:(CGSize)size hardness:(CGFloat)hardness roundness:(CGFloat)roundness -{ - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray(); - CGContextRef ctx = CGBitmapContextCreate(NULL, (NSInteger)size.width, (NSInteger)size.height, 8, (NSInteger)size.width, colorspace, kCGImageAlphaNone); - - CGContextSetGrayFillColor(ctx, 0.4f, 1.0f); - CGContextFillRect(ctx, CGRectMake(0, 0, size.width, size.height)); - - CGContextTranslateCTM(ctx, 0.5f * size.width, 0.5f * size.height); - CGContextScaleCTM(ctx, roundness, 1.0f); - CGContextTranslateCTM(ctx, -0.5f * size.width, -0.5f * size.height); - - NSArray *colors = @[(__bridge id) [UIColor whiteColor].CGColor, (__bridge id) [UIColor blackColor].CGColor]; - const CGFloat locations[] = {0.0, 1.0}; - - CGGradientRef gradientRef = CGGradientCreateWithColors(colorspace, (__bridge CFArrayRef) colors, locations); - CGPoint center = CGPointMake(size.width / 2, size.height / 2); - - CGFloat maxRadius = size.width / 2; - CGFloat hFactor = hardness * 0.99; - CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation |kCGGradientDrawsAfterEndLocation; - CGContextDrawRadialGradient(ctx, gradientRef, center, hFactor * maxRadius, center, maxRadius, options); - - CGImageRef image = CGBitmapContextCreateImage(ctx); - - CGContextRelease(ctx); - CGColorSpaceRelease(colorspace); - CGGradientRelease(gradientRef); - - return image; -} - -- (CGImageRef)stampRef -{ - static CGImageRef image = NULL; - - if (image == NULL) - image = [self generateEllipticalStampForSize:TGPaintBrushTextureSize hardness:TGPaintEllipticalBrushHardness roundness:TGPaintEllipticalBrushRoundness]; - - return image; -} - -- (CGImageRef)previewStampRef -{ - if (_previewStampRef == NULL) - { - _previewStampRef = [self generateEllipticalStampForSize:TGPaintBrushPreviewTextureSize hardness:TGPaintEllipticalBrushHardness roundness:TGPaintEllipticalBrushRoundness]; - } - - return _previewStampRef; -} - -static UIImage *ellipticalBrushPreviewImage = nil; - -- (UIImage *)previewImage -{ - return ellipticalBrushPreviewImage; -} - -- (void)setPreviewImage:(UIImage *)previewImage -{ - ellipticalBrushPreviewImage = previewImage; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.h b/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.h deleted file mode 100644 index 64778ccbd6e..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.h +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@interface TGPaintFaceDebugView : UIView - -- (void)setFaces:(NSArray *)faces paintingSize:(CGSize)paintingSize originalSize:(CGSize)originalSize; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.m b/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.m deleted file mode 100644 index aa55bd3c8af..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintFaceDebugView.m +++ /dev/null @@ -1,65 +0,0 @@ -#import "TGPaintFaceDebugView.h" - -#import "LegacyComponentsInternal.h" - -#import "TGPaintFaceDetector.h" - -@interface TGPaintFaceView : UIView - -- (instancetype)initWithFace:(TGPaintFace *)face paintingSize:(CGSize)paintingSize originalSize:(CGSize)originalSize; - -@end - -@implementation TGPaintFaceDebugView - -- (void)setFaces:(NSArray *)faces paintingSize:(CGSize)paintingSize originalSize:(CGSize)originalSize -{ - self.backgroundColor = UIColorRGBA(0x00ff00, 0.2f); - - for (UIView *view in self.subviews) - [view removeFromSuperview]; - - for (TGPaintFace *face in faces) - { - TGPaintFaceView *view = [[TGPaintFaceView alloc] initWithFace:face paintingSize:paintingSize originalSize:originalSize]; - [self addSubview:view]; - } -} - -@end - - -@implementation TGPaintFaceView - -- (instancetype)initWithFace:(TGPaintFace *)face paintingSize:(CGSize)paintingSize originalSize:(CGSize)originalSize -{ - CGRect bounds = [TGPaintFaceUtils transposeRect:face.bounds paintingSize:paintingSize originalSize:originalSize]; - self = [super initWithFrame:bounds]; - if (self != nil) - { - UIView *background = [[UIView alloc] initWithFrame:self.bounds]; - background.backgroundColor = UIColorRGBA(0xff0000, 0.4f); - [self addSubview:background]; - - void (^createViewForFeature)(TGPaintFaceFeature *) = ^(TGPaintFaceFeature *feature) - { - if (feature == nil) - return; - - CGPoint position = [TGPaintFaceUtils transposePoint:feature.position paintingSize:paintingSize originalSize:originalSize]; - - UIView *view = [[UIView alloc] initWithFrame:CGRectMake(position.x - 10.0f - self.frame.origin.x, position.y - 10.0f - self.frame.origin.y, 20, 20)]; - view.backgroundColor = UIColorRGBA(0x0000ff, 0.5f); - [self addSubview:view]; - }; - - createViewForFeature(face.leftEye); - createViewForFeature(face.rightEye); - createViewForFeature(face.mouth); - - background.transform = CGAffineTransformMakeRotation(face.angle); - } - return self; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintInput.h b/submodules/LegacyComponents/Sources/TGPaintInput.h deleted file mode 100644 index 11ba0356693..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintInput.h +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import - -@class TGPaintState; -@class TGPaintPanGestureRecognizer; - -@interface TGPaintInput : NSObject - -@property (nonatomic, assign) CGAffineTransform transform; - -- (void)gestureBegan:(TGPaintPanGestureRecognizer *)recognizer; -- (void)gestureMoved:(TGPaintPanGestureRecognizer *)recognizer; -- (void)gestureEnded:(TGPaintPanGestureRecognizer *)recognizer; -- (void)gestureCanceled:(TGPaintPanGestureRecognizer *)recognizer; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintInput.m b/submodules/LegacyComponents/Sources/TGPaintInput.m deleted file mode 100644 index 53ba273bf01..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintInput.m +++ /dev/null @@ -1,225 +0,0 @@ -#import "TGPaintInput.h" -#import - -#import "LegacyComponentsInternal.h" - -#import "TGPaintPanGestureRecognizer.h" - -#import "TGPainting.h" -#import "TGPaintPath.h" -#import "TGPaintState.h" -#import "TGPaintCanvas.h" -#import "TGPaintBrush.h" -#import - -@interface TGPaintInput () -{ - bool _first; - bool _moved; - bool _clearBuffer; - - CGPoint _lastLocation; - CGFloat _lastRemainder; - CGFloat _lastPressureRemainder; - CGFloat _lastAngle; - - TGPaintPoint *_points[3]; - NSInteger _pointsCount; -} -@end - -@implementation TGPaintInput - -- (CGPoint)_location:(CGPoint)location inView:(UIView *)view -{ - location.y = view.bounds.size.height - location.y; - - CGAffineTransform inverted = CGAffineTransformInvert(_transform); - CGPoint transformed = CGPointApplyAffineTransform(location, inverted); - - return transformed; -} - -- (void)smoothenAndPaintPoints:(TGPaintCanvas *)canvas ended:(bool)ended -{ - NSMutableArray *points = [[NSMutableArray alloc] init]; - - TGPaintPoint *prev2 = _points[0]; - TGPaintPoint *prev1 = _points[1]; - TGPaintPoint *cur = _points[2]; - - CGPoint midPoint1 = TGPaintMultiplyPoint(TGPaintAddPoints(prev1.CGPoint, prev2.CGPoint), 0.5f); - CGFloat midPressure1 = (prev1.z + prev2.z) * 0.5f; - CGPoint midPoint2 = TGPaintMultiplyPoint(TGPaintAddPoints(cur.CGPoint, prev1.CGPoint), 0.5f); - CGFloat midPressure2 = (cur.z + prev1.z) * 0.5f; - - NSInteger segmentDistance = 2; - CGFloat distance = TGPaintDistance(midPoint1, midPoint2); - NSInteger numberOfSegments = (NSInteger)MIN(72, MAX(floor(distance / segmentDistance), 36)); - - CGFloat t = 0.0f; - CGFloat step = 1.0f / numberOfSegments; - for (NSInteger j = 0; j < numberOfSegments; j++) - { - CGFloat f1 = pow(1 - t, 2); - CGFloat f2 = 2.0 * (1 - t) * t; - CGFloat f3 = t * t; - - CGPoint pos = TGPaintAddPoints(TGPaintAddPoints(TGPaintMultiplyPoint(midPoint1, f1), TGPaintMultiplyPoint(prev1.CGPoint, f2)), TGPaintMultiplyPoint(midPoint2, f3)); - CGFloat pressure = (midPressure1 * f1 + prev1.z * f2) + (midPressure2 * f3); - TGPaintPoint *newPoint = [TGPaintPoint pointWithCGPoint:pos z:pressure]; - if (_first) - { - newPoint.edge = true; - _first = false; - } - [points addObject:newPoint]; - t += step; - } - - TGPaintPoint *finalPoint = [TGPaintPoint pointWithCGPoint:midPoint2 z:midPressure2]; - if (ended) - finalPoint.edge = true; - [points addObject:finalPoint]; - - TGPaintPath *path = [[TGPaintPath alloc] initWithPoints:points]; - [self paintPath:path inCanvas:canvas]; - - for (int i = 0; i < 2; i++) - { - _points[i] = _points[i + 1]; - } - - if (ended) - _pointsCount = 0; - else - _pointsCount = 2; -} - -- (void)gestureBegan:(TGPaintPanGestureRecognizer *)recognizer -{ - _moved = false; - _first = true; - - CGPoint location = [self _location:[recognizer locationInView:recognizer.view] inView:recognizer.view]; - _lastLocation = location; - - TGPaintPoint *point = [TGPaintPoint pointWithX:location.x y:location.y z:1.0f]; - _points[0] = point; - _pointsCount = 1; - - _clearBuffer = true; -} - -- (void)gestureMoved:(TGPaintPanGestureRecognizer *)recognizer -{ - TGPaintCanvas *canvas = (TGPaintCanvas *)recognizer.view; - CGPoint location = [self _location:[recognizer locationInView:recognizer.view] inView:recognizer.view]; - CGFloat distanceMoved = TGPaintDistance(location, _lastLocation); - - if (distanceMoved < 8.0f) - return; - - const CGFloat speedFactor = 3.0f; - CGPoint velocity = [recognizer velocityInView:recognizer.view]; - CGFloat speed = TGPaintDistance(CGPointZero, velocity) / 1000.0f; - CGFloat pressure = TGPaintSineCurve(1.0f - MIN(speedFactor, speed) / speedFactor); - pressure = 1.0f - pressure; - - if (_pointsCount != 0) - pressure = (pressure + _points[_pointsCount - 1].z) / 2.0f; - - TGPaintPoint *point = [TGPaintPoint pointWithX:location.x y:location.y z:pressure]; - _points[_pointsCount++] = point; - - if (_pointsCount == 3) - { - CGPoint prev = _points[1].CGPoint; - CGPoint cur = _points[2].CGPoint; - _lastAngle = atan2(cur.y - prev.y, cur.x - prev.x); - - [self smoothenAndPaintPoints:canvas ended:false]; - _moved = true; - } - - _lastLocation = location; -} - -- (void)gestureEnded:(TGPaintPanGestureRecognizer *)recognizer -{ - TGPaintCanvas *canvas = (TGPaintCanvas *)recognizer.view; - TGPainting *painting = canvas.painting; - - CGPoint location = [self _location:[recognizer locationInView:recognizer.view] inView:recognizer.view]; - if (!_moved) - { - TGPaintPoint *point = [TGPaintPoint pointWithX:location.x y:location.y z:1.0]; - point.edge = true; - - TGPaintPath *path = [[TGPaintPath alloc] initWithPoint:point]; - [self paintPath:path inCanvas:canvas]; - } - else - { - [self smoothenAndPaintPoints:canvas ended:true]; - - if (canvas.state.brush.arrow) { - CGFloat arrowLength = canvas.state.weight * 4.5; - CGFloat angle = _lastAngle; - - TGPaintPoint *tip = [TGPaintPoint pointWithX:location.x y:location.y z:0.8]; - TGPaintPoint *leftTip = [TGPaintPoint pointWithX:location.x + cos(angle - M_PI_4 * 3) * arrowLength y:location.y + sin(angle - M_PI_4 * 3.2) * arrowLength z:1.0]; - leftTip.edge = true; - TGPaintPath *left = [[TGPaintPath alloc] initWithPoints:@[tip, leftTip]]; - [self paintPath:left inCanvas:canvas]; - - TGPaintPoint *rightTip = [TGPaintPoint pointWithX:location.x + cos(angle + M_PI_4 * 3) * arrowLength y:location.y + sin(angle + M_PI_4 * 3.2) * arrowLength z:1.0]; - rightTip.edge = true; - TGPaintPath *right = [[TGPaintPath alloc] initWithPoints:@[tip, rightTip]]; - [self paintPath:right inCanvas:canvas]; - } - } - - _pointsCount = 0; - - [painting commitStrokeWithColor:canvas.state.color erase:canvas.state.isEraser]; -} - -- (void)gestureCanceled:(UIGestureRecognizer *)recognizer -{ - TGPaintCanvas *canvas = (TGPaintCanvas *) recognizer.view; - TGPainting *painting = canvas.painting; - - [painting performAsynchronouslyInContext:^{ - painting.activePath = nil; - [canvas draw]; - }]; -} - -- (void)paintPath:(TGPaintPath *)path inCanvas:(TGPaintCanvas *)canvas -{ - path.color = canvas.state.color; - path.action = canvas.state.isEraser ? TGPaintActionErase : TGPaintActionDraw; - path.brush = canvas.state.brush; - path.baseWeight = canvas.state.weight; - - if (_clearBuffer) { - _lastRemainder = 0.0f; - _lastPressureRemainder = 0.0f; - } - - path.remainder = _lastRemainder; - path.pressureRemainder = _lastPressureRemainder; - - [canvas.painting paintStroke:path clearBuffer:_clearBuffer completion:^ - { - TGDispatchOnMainThread(^ - { - _lastRemainder = path.remainder; - _lastPressureRemainder = path.pressureRemainder; - _clearBuffer = false; - }); - }]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintNeonBrush.h b/submodules/LegacyComponents/Sources/TGPaintNeonBrush.h deleted file mode 100644 index d902dc245c6..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintNeonBrush.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPaintBrush.h" - -@interface TGPaintNeonBrush : TGPaintBrush - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintNeonBrush.m b/submodules/LegacyComponents/Sources/TGPaintNeonBrush.m deleted file mode 100644 index 83232780f13..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintNeonBrush.m +++ /dev/null @@ -1,113 +0,0 @@ -#import "TGPaintNeonBrush.h" - -#import "LegacyComponentsInternal.h" - -const CGFloat TGPaintNeonBrushSolidFraction = 0.41f; -const CGFloat TGPaintNeonBrushBorderFraction = 0.036f; - -@implementation TGPaintNeonBrush - -- (CGFloat)spacing -{ - return 0.07f; -} - -- (CGFloat)alpha -{ - return 0.7f; -} - -- (CGFloat)angle -{ - return 0.0f; -} - -- (CGFloat)scale -{ - return 1.45f; -} - -- (bool)lightSaber -{ - return true; -} - -- (CGImageRef)generateNeonStampForSize:(CGSize)size -{ - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); - CGContextRef ctx = CGBitmapContextCreate(NULL, (NSInteger)size.width, (NSInteger)size.height, 8, (NSInteger)size.width * 4, colorspace, kCGImageAlphaPremultipliedLast); - - CGPoint center = CGPointMake(size.width / 2, size.height / 2); - - NSArray *redColors = @ - [ - (__bridge id)UIColorRGB(0x440000).CGColor, - (__bridge id)UIColorRGB(0x440000).CGColor, - (__bridge id)[UIColor blackColor].CGColor - ]; - const CGFloat redLocations[] = { 0.0f, 0.54f, 1.0f }; - CGGradientRef gradientRef = CGGradientCreateWithColors(colorspace, (__bridge CFArrayRef)redColors, redLocations); - CGFloat redMaxRadius = size.width / 2; - CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; - - CGContextDrawRadialGradient(ctx, gradientRef, center, 0, center, redMaxRadius, options); - CGGradientRelease(gradientRef); - - CGContextSetBlendMode(ctx, kCGBlendModeScreen); - - CGFloat border = floor(size.width / 2 * TGPaintNeonBrushBorderFraction); - - CGFloat blueRadius = floor(size.width / 2 * TGPaintNeonBrushSolidFraction - border); - CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor); - CGContextAddEllipseInRect(ctx, CGRectMake(size.width / 2.0f - blueRadius, size.height / 2.0f - blueRadius, blueRadius * 2, blueRadius * 2)); - CGContextFillPath(ctx); - - CGFloat greenRadius = blueRadius + border + 1; - CGContextSetLineWidth(ctx, border * 3); - CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor); - CGContextAddEllipseInRect(ctx, CGRectMake(size.width / 2.0f - greenRadius, size.height / 2.0f - greenRadius, greenRadius * 2, greenRadius * 2)); - CGContextStrokePath(ctx); - - CGImageRef image = CGBitmapContextCreateImage(ctx); - - CGContextRelease(ctx); - CGColorSpaceRelease(colorspace); - - return image; -} - -- (CGImageRef)stampRef -{ - static CGImageRef image = NULL; - - if (image == NULL) - { - image = [self generateNeonStampForSize:TGPaintBrushTextureSize]; - } - - return image; -} - -- (CGImageRef)previewStampRef -{ - if (_previewStampRef == NULL) - { - _previewStampRef = [self generateNeonStampForSize:TGPaintBrushPreviewTextureSize]; - } - - return _previewStampRef; -} - -static UIImage *neonBrushPreviewImage = nil; - -- (UIImage *)previewImage -{ - return neonBrushPreviewImage; -} - -- (void)setPreviewImage:(UIImage *)previewImage -{ - neonBrushPreviewImage = previewImage; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.h b/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.h deleted file mode 100644 index be917f7891c..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface TGPaintPanGestureRecognizer : UIPanGestureRecognizer - -@property (nonatomic, copy) bool (^shouldRecognizeTap)(void); -@property (nonatomic) NSSet *touches; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.m b/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.m deleted file mode 100644 index 15fb86a1446..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintPanGestureRecognizer.m +++ /dev/null @@ -1,38 +0,0 @@ -#import "TGPaintPanGestureRecognizer.h" - -#import - -@implementation TGPaintPanGestureRecognizer - -- (void)touchesBegan:(NSSet *)inTouches withEvent:(UIEvent *)event -{ - _touches = [inTouches copy]; - [super touchesBegan:inTouches withEvent:event]; - - if (inTouches.count == 1 && self.shouldRecognizeTap != nil && self.shouldRecognizeTap()) - self.state = UIGestureRecognizerStateBegan; -} - -- (void)touchesMoved:(NSSet *)inTouches withEvent:(UIEvent *)event -{ - _touches = [inTouches copy]; - if (inTouches.count > 1) { - self.state = UIGestureRecognizerStateCancelled; - } else { - [super touchesMoved:inTouches withEvent:event]; - } -} - -- (void)touchesEnded:(NSSet *)inTouches withEvent:(UIEvent *)event -{ - _touches = [inTouches copy]; - [super touchesEnded:inTouches withEvent:event]; -} - -- (void)touchesCancelled:(NSSet *)inTouches withEvent:(UIEvent *)event -{ - _touches = [inTouches copy]; - [super touchesCancelled:inTouches withEvent:event]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintPath.h b/submodules/LegacyComponents/Sources/TGPaintPath.h deleted file mode 100644 index 953112369a9..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintPath.h +++ /dev/null @@ -1,53 +0,0 @@ -#import -#import -#import - -@interface TGPaintPoint : NSObject - -@property (nonatomic, assign) CGFloat x; -@property (nonatomic, assign) CGFloat y; -@property (nonatomic, assign) CGFloat z; - -@property (nonatomic, assign) bool edge; - -- (TGPaintPoint *)add:(TGPaintPoint *)point; -- (TGPaintPoint *)subtract:(TGPaintPoint *)point; -- (TGPaintPoint *)multiplyByScalar:(CGFloat)scalar; - -- (CGFloat)distanceTo:(TGPaintPoint *)point; -- (TGPaintPoint *)normalize; - -- (CGPoint)CGPoint; - -+ (instancetype)pointWithX:(CGFloat)x y:(CGFloat)y z:(CGFloat)z; -+ (instancetype)pointWithCGPoint:(CGPoint)point z:(CGFloat)z; - -@end - - -typedef enum -{ - TGPaintActionDraw, - TGPaintActionErase -} TGPaintAction; - -@class TGPaintBrush; - -@interface TGPaintPath : NSObject - -@property (nonatomic, strong) NSArray *points; - -@property (nonatomic, strong) UIColor *color; -@property (nonatomic, assign) TGPaintAction action; -@property (nonatomic, assign) CGFloat baseWeight; -@property (nonatomic, strong) TGPaintBrush *brush; - -@property (nonatomic, assign) CGFloat remainder; -@property (nonatomic, assign) CGFloat pressureRemainder; - -- (instancetype)initWithPoint:(TGPaintPoint *)point; -- (instancetype)initWithPoints:(NSArray *)points; -- (void)addPoint:(TGPaintPoint *)point; - -@end - diff --git a/submodules/LegacyComponents/Sources/TGPaintPath.m b/submodules/LegacyComponents/Sources/TGPaintPath.m deleted file mode 100644 index 3c28de7b3c8..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintPath.m +++ /dev/null @@ -1,130 +0,0 @@ -#import "TGPaintPath.h" - -@implementation TGPaintPoint - -+ (instancetype)pointWithX:(CGFloat)x y:(CGFloat)y z:(CGFloat)z -{ - TGPaintPoint *point = [[TGPaintPoint alloc] init]; - point.x = x; - point.y = y; - point.z = z; - return point; -} - -+ (instancetype)pointWithCGPoint:(CGPoint)inPoint z:(CGFloat)z -{ - TGPaintPoint *point = [[TGPaintPoint alloc] init]; - point.x = inPoint.x; - point.y = inPoint.y; - point.z = z; - return point; -} - -- (instancetype)copyWithZone:(NSZone *)__unused zone -{ - return [TGPaintPoint pointWithX:_x y:_y z:_z]; -} - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPaintPoint *point = (TGPaintPoint *)object; - return (_x == point.x && _y == point.y && _z == point.z); -} - -- (CGPoint)CGPoint -{ - return CGPointMake(_x, _y); -} - -- (TGPaintPoint *)add:(TGPaintPoint *)point -{ - return [TGPaintPoint pointWithX:_x + point.x y:_y + point.y z:_z + point.z]; -} - -- (TGPaintPoint *)subtract:(TGPaintPoint *)point -{ - return [TGPaintPoint pointWithX:_x - point.x y:_y - point.y z:_z - point.z]; -} - -- (TGPaintPoint *)multiplyByScalar:(CGFloat)scalar -{ - return [TGPaintPoint pointWithX:_x * scalar y:_y * scalar z:_z * scalar]; -} - -- (TGPaintPoint *)normalize -{ - return [self multiplyByScalar:1.0f / [self magnitude]]; -} - -- (CGFloat)magnitude -{ - return sqrt(_x * _x + _y * _y + _z * _z); -} - -- (CGFloat)distanceTo:(TGPaintPoint *)point -{ - CGFloat xD = _x - point.x; - CGFloat yD = _y - point.y; - CGFloat zD = _z - point.z; - - return sqrt(xD * xD + yD * yD + zD * zD); -} - -@end - - -@interface TGPaintPath () -{ - NSMutableArray *_points; -} -@end - -@implementation TGPaintPath - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - _points = [[NSMutableArray alloc] init]; - } - return self; -} - -- (instancetype)initWithPoint:(TGPaintPoint *)point -{ - self = [self init]; - if (self != nil) - { - [_points addObject:point]; - } - return self; -} - -- (instancetype)initWithPoints:(NSArray *)points -{ - self = [self init]; - if (self != nil) - { - [_points addObjectsFromArray:points]; - } - return self; -} - -- (NSArray *)points -{ - return [_points copy]; -} - -- (void)addPoint:(TGPaintPoint *)point -{ - [_points addObject:point]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintRadialBrush.h b/submodules/LegacyComponents/Sources/TGPaintRadialBrush.h deleted file mode 100644 index ac63275ce23..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintRadialBrush.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPaintBrush.h" - -@interface TGPaintRadialBrush : TGPaintBrush - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintRadialBrush.m b/submodules/LegacyComponents/Sources/TGPaintRadialBrush.m deleted file mode 100644 index 2bf80678af3..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintRadialBrush.m +++ /dev/null @@ -1,85 +0,0 @@ -#import "TGPaintRadialBrush.h" - -const CGFloat TGPaintRadialBrushHardness = 0.92f; - -@implementation TGPaintRadialBrush - -- (CGFloat)spacing -{ - return 0.15f; -} - -- (CGFloat)alpha -{ - return 0.85f; -} - -- (CGFloat)angle -{ - return 0.0f; -} - -//- (CGFloat)dynamic -//{ -// return 0.75f; -//} - -- (CGImageRef)generateRadialStampForSize:(CGSize)size hardness:(CGFloat)hardness -{ - CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray(); - CGContextRef ctx = CGBitmapContextCreate(NULL, (NSInteger)size.width, (NSInteger)size.height, 8, (NSInteger)size.width, colorspace, kCGImageAlphaNone); - - CGContextSetGrayFillColor(ctx, 0.0f, 1.0f); - CGContextFillRect(ctx, CGRectMake(0, 0, size.width, size.height)); - - NSArray *colors = @[(__bridge id) [UIColor whiteColor].CGColor, (__bridge id) [UIColor blackColor].CGColor]; - const CGFloat locations[] = {0.0, 1.0}; - - CGGradientRef gradientRef = CGGradientCreateWithColors(colorspace, (__bridge CFArrayRef) colors, locations); - CGPoint center = CGPointMake(size.width / 2, size.height / 2); - - CGFloat maxRadius = size.width / 2; - CGFloat hFactor = hardness * 0.99; - CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation |kCGGradientDrawsAfterEndLocation; - CGContextDrawRadialGradient(ctx, gradientRef, center, hFactor * maxRadius, center, maxRadius, options); - - CGImageRef image = CGBitmapContextCreateImage(ctx); - - CGContextRelease(ctx); - CGColorSpaceRelease(colorspace); - CGGradientRelease(gradientRef); - - return image; -} - -- (CGImageRef)stampRef -{ - static CGImageRef image = NULL; - - if (image == NULL) - image = [self generateRadialStampForSize:TGPaintBrushTextureSize hardness:TGPaintRadialBrushHardness]; - - return image; -} - -- (CGImageRef)previewStampRef -{ - if (_previewStampRef == NULL) - _previewStampRef = [self generateRadialStampForSize:TGPaintBrushPreviewTextureSize hardness:TGPaintRadialBrushHardness]; - - return _previewStampRef; -} - -static UIImage *radialBrushPreviewImage = nil; - -- (UIImage *)previewImage -{ - return radialBrushPreviewImage; -} - -- (void)setPreviewImage:(UIImage *)previewImage -{ - radialBrushPreviewImage = previewImage; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintRender.h b/submodules/LegacyComponents/Sources/TGPaintRender.h deleted file mode 100644 index 5c91d9b87c5..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintRender.h +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import - -@class TGPaintPath; - -@interface TGPaintRenderState : NSObject - -- (void)reset; - -@end - -@interface TGPaintRender : NSObject - -+ (CGRect)renderPath:(TGPaintPath *)path renderState:(TGPaintRenderState *)renderState; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintRender.m b/submodules/LegacyComponents/Sources/TGPaintRender.m deleted file mode 100644 index 4f95a878879..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintRender.m +++ /dev/null @@ -1,325 +0,0 @@ -#import "TGPaintRender.h" - -#include - -#import "TGPaintBrush.h" -#import "TGPaintPath.h" -#import - -const NSInteger TGPaintRenderStateDefaultSize = 256; - -@interface TGPaintRenderState () -{ - NSUInteger _allocatedCount; -} - -@property (nonatomic, assign) CGFloat brushWeight; -@property (nonatomic, assign) CGFloat brushDynamic; -@property (nonatomic, assign) CGFloat spacing; -@property (nonatomic, assign) CGFloat alpha; -@property (nonatomic, assign) CGFloat angle; -@property (nonatomic, assign) CGFloat scale; - -@property (nonatomic, readonly) CGFloat *values; -@property (nonatomic, readonly) NSUInteger count; - -@property (nonatomic, assign) CGFloat remainder; -@property (nonatomic, assign) CGFloat pressureRemainder; - -- (void)reset; - -@end - -@implementation TGPaintRenderState - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - _values = NULL; - } - return self; -} - -- (void)dealloc -{ - if (_values != NULL) - { - free(_values); - _values = NULL; - } -} - -- (void)prepare -{ - if (_values != NULL) - { - free(_values); - _values = NULL; - } - - _count = 0; - _allocatedCount = TGPaintRenderStateDefaultSize; - _values = malloc(sizeof(CGFloat) * TGPaintRenderStateDefaultSize * 5); -} - -- (void)appendValuesCount:(NSUInteger)count -{ - NSUInteger newTotalCount = _count + count; - - if (newTotalCount > _allocatedCount || _values == NULL) - { - if (_values != NULL) - { - free(_values); - _values = NULL; - } - - NSInteger newCount = MAX(_allocatedCount * 2, TGPaintRenderStateDefaultSize); - _values = malloc(sizeof(CGFloat) * newCount * 5); - _allocatedCount = newCount; - } - - _count = newTotalCount; -} - -- (void)addPoint:(CGPoint)point size:(CGFloat)size angle:(CGFloat)angle alpha:(CGFloat)alpha index:(NSInteger)index -{ - NSInteger column = index * 5; - _values[column] = point.x; - _values[column + 1] = point.y; - _values[column + 2] = size; - _values[column + 3] = angle; - _values[column + 4] = alpha; -} - -- (void)reset -{ - _count = 0; - _allocatedCount = 0; - if (_values != NULL) - { - free(_values); - _values = NULL; - } - - _remainder = 0; - _pressureRemainder = 0; -} - -@end - -@implementation TGPaintRender - -typedef struct -{ - GLfloat x; - GLfloat y; - GLfloat s; - GLfloat t; - GLfloat a; -} vertexData; - -+ (void)_paintStamp:(TGPaintPoint *)point state:(TGPaintRenderState *)state -{ - CGFloat brushSize = state.brushWeight * state.scale; - CGFloat angleOffset = fabs(state.angle) > FLT_EPSILON ? state.angle : 0.0f; - CGFloat alpha = MIN(1.0f, state.alpha * 1.55f); - - [state prepare]; - [state appendValuesCount:4]; - for (NSInteger i = 0; i < 4; i++) { - [state addPoint:point.CGPoint size:brushSize angle:angleOffset alpha:alpha index:i]; - } -} - -+ (void)_paintFromPoint:(TGPaintPoint *)lastLocation toPoint:(TGPaintPoint *)location state:(TGPaintRenderState *)state -{ - CGFloat lastP = lastLocation.z; - CGFloat p = location.z; - CGFloat pDelta = p - lastP; - CGFloat pChange = 0.0f; - - CGFloat f, distance = TGPaintDistance(lastLocation.CGPoint, location.CGPoint); - CGPoint vector = TGPaintSubtractPoints(location.CGPoint, lastLocation.CGPoint); - CGPoint unitVector = CGPointMake(1.0f, 1.0f); - CGFloat vectorAngle = fabs(state.angle) > FLT_EPSILON ? state.angle : atan2(vector.y, vector.x); - - CGFloat brushWeight = state.brushWeight * state.scale; - CGFloat step = MAX(1.0f, state.spacing * brushWeight); - - CGFloat pressure = lastP + state.pressureRemainder; - CGFloat pressureStep = pressureStep = pDelta / ((distance - state.remainder) / step); - - if (distance > 0.0f) - unitVector = TGPaintMultiplyPoint(vector, 1.0f / distance); - - CGPoint start = TGPaintAddPoints(lastLocation.CGPoint, TGPaintMultiplyPoint(unitVector, state.remainder)); - - NSInteger i = state.count; - NSInteger count = (NSInteger)(ceil((distance - state.remainder) / step)); - - CGFloat boldenedAlpha = MIN(1.0f, state.alpha * 1.15f); - bool boldenFirst = lastLocation.edge; - bool boldenLast = location.edge; - - [state appendValuesCount:count]; - - for (f = state.remainder; f <= distance; f += step, pressure += pressureStep) - { - CGFloat alpha = boldenFirst ? boldenedAlpha : state.alpha; - CGFloat brushSize = MAX(1.0, brushWeight - state.brushDynamic * pressure * brushWeight); -// CGFloat brushSize = brushWeight; - [state addPoint:start size:brushSize angle:vectorAngle alpha:alpha index:i]; - - start = TGPaintAddPoints(start, TGPaintMultiplyPoint(unitVector, step)); - - boldenFirst = false; - - pChange += pressureStep; - - i++; - } -// NSLog(@"final pressure %f", pressure); - - if (boldenLast) - { - [state appendValuesCount:1]; - CGFloat brushSize = MAX(1.0, brushWeight - state.brushDynamic * pressure * brushWeight); - [state addPoint:location.CGPoint size:brushSize angle:vectorAngle alpha:boldenedAlpha index:i]; - } - - state.remainder = f - distance; - state.pressureRemainder = pChange - pDelta; -} - -+ (CGRect)_drawWithState:(TGPaintRenderState *)state -{ - vertexData *vertexD = calloc(sizeof(vertexData), state.count * 4 + (state.count - 1) * 2); - CGRect dataBounds = CGRectZero; - - int n = 0; - for (NSUInteger i = 0; i < state.count; i++) - { - NSInteger column = i * 5; - - CGPoint result = CGPointMake(state.values[column], state.values[column + 1]); - CGFloat size = state.values[column + 2] / 2; - CGFloat angle = state.values[column + 3]; - CGFloat alpha = state.values[column + 4]; - - CGRect rect = CGRectMake(result.x - size, result.y - size, size*2, size*2); - CGPoint a = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)); - CGPoint b = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)); - CGPoint c = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)); - CGPoint d = CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)); - - CGPoint center = TGPaintCenterOfRect(rect); - CGAffineTransform t = CGAffineTransformMakeTranslation(center.x, center.y); - t = CGAffineTransformRotate(t, angle); - t = CGAffineTransformTranslate(t, -center.x, -center.y); - - a = CGPointApplyAffineTransform(a, t); - b = CGPointApplyAffineTransform(b, t); - c = CGPointApplyAffineTransform(c, t); - d = CGPointApplyAffineTransform(d, t); - - CGRect boxBounds = CGRectApplyAffineTransform(rect, t); - dataBounds = TGPaintUnionRect(dataBounds, CGRectIntegral(boxBounds)); - - if (n != 0) - { - vertexD[n].x = (GLfloat)a.x; - vertexD[n].y = (GLfloat)a.y; - vertexD[n].s = (GLfloat)0; - vertexD[n].t = (GLfloat)0; - vertexD[n].a = (GLfloat)alpha; - n++; - } - - vertexD[n].x = (GLfloat)a.x; - vertexD[n].y = (GLfloat)a.y; - vertexD[n].s = (GLfloat)0; - vertexD[n].t = (GLfloat)0; - vertexD[n].a = (GLfloat)alpha; - n++; - - vertexD[n].x = (GLfloat)b.x; - vertexD[n].y = (GLfloat)b.y; - vertexD[n].s = (GLfloat)1; - vertexD[n].t = (GLfloat)0; - vertexD[n].a = (GLfloat)alpha; - n++; - - vertexD[n].x = (GLfloat)c.x; - vertexD[n].y = (GLfloat)c.y; - vertexD[n].s = (GLfloat)0; - vertexD[n].t = (GLfloat)1; - vertexD[n].a = (GLfloat)alpha; - n++; - - vertexD[n].x = (GLfloat)d.x; - vertexD[n].y = (GLfloat)d.y; - vertexD[n].s = (GLfloat)1; - vertexD[n].t = (GLfloat)1; - vertexD[n].a = (GLfloat)alpha; - n++; - - if (i != (state.count - 1)) - { - vertexD[n].x = (GLfloat)d.x; - vertexD[n].y = (GLfloat)d.y; - vertexD[n].s = (GLfloat)1; - vertexD[n].t = (GLfloat)1; - vertexD[n].a = (GLfloat)alpha; - n++; - } - } - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertexData), &vertexD[0].x); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_TRUE, sizeof(vertexData), &vertexD[0].s); - glEnableVertexAttribArray(1); - glVertexAttribPointer(2, 1, GL_FLOAT, GL_TRUE, sizeof(vertexData), &vertexD[0].a); - glEnableVertexAttribArray(2); - - glDrawArrays(GL_TRIANGLE_STRIP, 0, n); - - free(vertexD); - TGPaintHasGLError(); - - return dataBounds; -} - -+ (CGRect)renderPath:(TGPaintPath *)path renderState:(TGPaintRenderState *)renderState -{ - renderState.brushWeight = path.baseWeight; - renderState.brushDynamic = path.brush.dynamic; - renderState.spacing = path.brush.spacing; - renderState.alpha = path.brush.alpha; - renderState.angle = path.brush.angle; - renderState.scale = path.brush.scale; - - if (path.points.count == 1) - { - [self _paintStamp:path.points.lastObject state:renderState]; - } - else - { - NSArray *points = path.points; - [renderState prepare]; - - for (NSUInteger i = 0; i < points.count - 1; i++) - { - [self _paintFromPoint:points[i] toPoint:points[i + 1] state:renderState]; - } - } - - path.remainder = renderState.remainder; - path.pressureRemainder = renderState.pressureRemainder; - - return [self _drawWithState:renderState]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintShaderSet.h b/submodules/LegacyComponents/Sources/TGPaintShaderSet.h deleted file mode 100644 index 2d58ffd9c25..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintShaderSet.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface TGPaintShaderSet : NSObject - -+ (NSDictionary *)availableShaders; -+ (NSDictionary *)setup; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintShaderSet.m b/submodules/LegacyComponents/Sources/TGPaintShaderSet.m deleted file mode 100644 index be6e103daf4..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintShaderSet.m +++ /dev/null @@ -1,124 +0,0 @@ -#import "TGPaintShaderSet.h" - -#import "TGPaintShader.h" -#import - -@implementation TGPaintShaderSet - -+ (NSDictionary *)availableShaders -{ - return @ - { - @"brush": @ - { - @"vertex": @"Paint_Brush", - @"fragment": @"Paint_Brush", - @"attributes": @[ @"inPosition", @"inTexcoord", @"alpha" ], - @"uniforms" : @[ @"mvpMatrix", @"texture" ] - }, - - @"brushLight": @ - { - @"vertex": @"Paint_Brush", - @"fragment": @"Paint_BrushLight", - @"attributes": @[ @"inPosition", @"inTexcoord", @"alpha" ], - @"uniforms" : @[ @"mvpMatrix", @"texture" ] - }, - - @"brushLightPreview": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_BrushLightPreview", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"mask", @"color" ] - }, - - @"blit": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_Blit", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture" ] - }, - - @"blitWithMaskLight": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_BlitWithMaskLight", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask", @"color" ] - }, - - @"blitWithMask": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_BlitWithMask", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask", @"color" ] - }, - - @"blitWithEraseMask": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_BlitWithEraseMask", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask"] - }, - - @"compositeWithMask": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_CompositeWithMask", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask", @"color" ] - }, - - @"compositeWithMaskLight": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_CompositeWithMaskLight", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask", @"color" ] - }, - - @"compositeWithEraseMask": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_CompositeWithEraseMask", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture", @"mask" ] - }, - - @"nonPremultipliedBlit": @ - { - @"vertex": @"Paint_Blit", - @"fragment": @"Paint_NonPremultipliedBlit", - @"attributes": @[ @"inPosition", @"inTexcoord" ], - @"uniforms": @[ @"mvpMatrix", @"texture" ] - } - }; -} - -+ (NSDictionary *)setup -{ - NSDictionary *shaderSet = [self availableShaders]; - - NSMutableDictionary *shaders = [NSMutableDictionary dictionary]; - for (NSString *key in shaderSet.keyEnumerator) - { - NSDictionary *desc = shaderSet[key]; - NSString *vertex = desc[@"vertex"]; - NSString *fragment = desc[@"fragment"]; - NSArray *attributes = desc[@"attributes"]; - NSArray *uniforms = desc[@"uniforms"]; - - TGPaintShader *shader = [[TGPaintShader alloc] initWithVertexShader:vertex fragmentShader:fragment attributes:attributes uniforms:uniforms]; - shaders[key] = shader; - } - - TGPaintHasGLError(); - - return shaders; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintSlice.h b/submodules/LegacyComponents/Sources/TGPaintSlice.h deleted file mode 100644 index b63c71018ee..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintSlice.h +++ /dev/null @@ -1,15 +0,0 @@ -#import -#import - -@class TGPainting; - -@interface TGPaintSlice : NSObject - -@property (nonatomic, readonly) CGRect bounds; -@property (nonatomic, readonly) NSData *data; - -- (instancetype)initWithData:(NSData *)data bounds:(CGRect)bounds; - -- (instancetype)swappedSliceForPainting:(TGPainting *)painting; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintSlice.m b/submodules/LegacyComponents/Sources/TGPaintSlice.m deleted file mode 100644 index 2bcc290dcc3..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintSlice.m +++ /dev/null @@ -1,78 +0,0 @@ -#import "TGPaintSlice.h" - -#import - -#import "TGPainting.h" -#import - -@interface TGPaintSlice () -{ - NSData *_data; - NSString *_fileName; -} -@end - -@implementation TGPaintSlice - -- (instancetype)initWithData:(NSData *)data bounds:(CGRect)bounds -{ - self = [super init]; - if (self != nil) - { - _bounds = bounds; - _data = data; - _fileName = [self _generatefileName]; - - [[TGPaintSlice queue] dispatch:^ - { - [TGPaintGZipDeflate(_data) writeToFile:_fileName atomically:true]; - [[SQueue mainQueue] dispatch:^ - { - _data = nil; - }]; - }]; - } - return self; -} - -- (void)dealloc -{ - if (_fileName != nil) - [[NSFileManager defaultManager] removeItemAtPath:_fileName error:NULL]; -} - -- (instancetype)swappedSliceForPainting:(TGPainting *)painting -{ - NSData *paintingData = nil; - [painting imageDataForRect:self.bounds resultPaintingData:&paintingData]; - return [[TGPaintSlice alloc] initWithData:paintingData bounds:self.bounds]; -} - -- (NSData *)data -{ - if (_data != nil) - return _data; - else if (_fileName != nil) - return TGPaintGZipInflate([[NSData alloc] initWithContentsOfFile:_fileName]); - else - return nil; -} - -- (NSString *)_generatefileName -{ - static uint32_t identifier = 0; - return [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%u.slice", identifier++]]; -} - -+ (SQueue *)queue -{ - static dispatch_once_t onceToken; - static SQueue *queue; - dispatch_once(&onceToken, ^ - { - queue = [SQueue wrapConcurrentNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)]; - }); - return queue; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintState.h b/submodules/LegacyComponents/Sources/TGPaintState.h deleted file mode 100644 index 1df427cb0bb..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintState.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import - -@class TGPaintBrush; - -@interface TGPaintState : NSObject - -@property (nonatomic, strong) UIColor *color; -@property (nonatomic, assign, getter=isEraser) bool eraser; -@property (nonatomic, assign) CGFloat weight; -@property (nonatomic, strong) TGPaintBrush *brush; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintState.m b/submodules/LegacyComponents/Sources/TGPaintState.m deleted file mode 100644 index 74a129c6be7..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintState.m +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPaintState.h" - -@implementation TGPaintState - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintSwatch.h b/submodules/LegacyComponents/Sources/TGPaintSwatch.h deleted file mode 100644 index c98faa1d98a..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintSwatch.h +++ /dev/null @@ -1,12 +0,0 @@ -#import -#import - -@interface TGPaintSwatch : NSObject - -@property (nonatomic, readonly) UIColor *color; -@property (nonatomic, readonly) CGFloat colorLocation; -@property (nonatomic, readonly) CGFloat brushWeight; - -+ (instancetype)swatchWithColor:(UIColor *)color colorLocation:(CGFloat)colorLocation brushWeight:(CGFloat)brushWeight; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintSwatch.m b/submodules/LegacyComponents/Sources/TGPaintSwatch.m deleted file mode 100644 index 3cb8fb3e4b6..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintSwatch.m +++ /dev/null @@ -1,27 +0,0 @@ -#import "TGPaintSwatch.h" - -@implementation TGPaintSwatch - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPaintSwatch *swatch = (TGPaintSwatch *)object; - return [swatch.color isEqual:self.color] && fabs(swatch.colorLocation - self.colorLocation) < FLT_EPSILON && fabs(swatch.brushWeight - self.brushWeight) < FLT_EPSILON; -} - -+ (instancetype)swatchWithColor:(UIColor *)color colorLocation:(CGFloat)colorLocation brushWeight:(CGFloat)brushWeight -{ - TGPaintSwatch *swatch = [[TGPaintSwatch alloc] init]; - swatch->_color = color; - swatch->_colorLocation = colorLocation; - swatch->_brushWeight = brushWeight; - - return swatch; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintTexture.h b/submodules/LegacyComponents/Sources/TGPaintTexture.h deleted file mode 100644 index b26ef2d70e9..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintTexture.h +++ /dev/null @@ -1,14 +0,0 @@ -#import -#import -#import - -@interface TGPaintTexture : NSObject - -@property (nonatomic, readonly) GLuint textureName; - -+ (instancetype)textureWithImage:(UIImage *)image forceRGB:(bool)forceRGB; - -- (instancetype)initWithCGImage:(CGImageRef)imageRef forceRGB:(bool)forceRGB; -- (void)cleanResources; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintTexture.m b/submodules/LegacyComponents/Sources/TGPaintTexture.m deleted file mode 100644 index 4265332f1ec..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintTexture.m +++ /dev/null @@ -1,110 +0,0 @@ -#import "TGPaintTexture.h" - -#import - -@interface TGPaintTexture () -{ - GLuint _textureName; - - GLubyte *_data; - GLsizei _size; - GLuint _width; - GLuint _height; - GLenum _type; - GLenum _format; - GLuint _rowByteSize; - GLuint _unpackAlign; -} -@end - -@implementation TGPaintTexture - -+ (instancetype)textureWithImage:(UIImage *)image forceRGB:(bool)forceRGB -{ - return [[TGPaintTexture alloc] initWithCGImage:image.CGImage forceRGB:forceRGB]; -} - -- (instancetype)initWithCGImage:(CGImageRef)imageRef forceRGB:(bool)forceRGB -{ - self = [super init]; - if (self != nil) - { - [self _loadTextureFromCGImage:imageRef forceRGB:forceRGB]; - } - return self; -} - -- (void)dealloc -{ - if (_data != NULL) - free(_data); - - TGPaintHasGLError(); -} - -- (void)cleanResources -{ - if (_textureName == 0) - return; - - glDeleteTextures(1, &_textureName); - _textureName = 0; -} - -- (void)_loadTextureFromCGImage:(CGImageRef)image forceRGB:(bool)forceRGB -{ - bool isAlpha = forceRGB ? false : CGImageGetBitsPerPixel(image) == 8; - - _width = (GLuint) CGImageGetWidth(image); - _height = (GLuint) CGImageGetHeight(image); - _unpackAlign = isAlpha ? 1 : 4; - _rowByteSize = _width * _unpackAlign; - _data = malloc(_height * _rowByteSize); - _type = GL_UNSIGNED_BYTE; - _format = isAlpha ? GL_ALPHA : GL_RGBA; - - CGColorSpaceRef colorSpaceRef = isAlpha ? CGColorSpaceCreateDeviceGray() : CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(_data, _width, _height, 8, _rowByteSize, colorSpaceRef, (isAlpha ? kCGImageAlphaNone : kCGImageAlphaPremultipliedLast)); - CGContextSetBlendMode(context, kCGBlendModeCopy); - CGContextDrawImage(context, CGRectMake(0.0, 0.0, _width, _height), image); - CGContextRelease(context); - - CGColorSpaceRelease(colorSpaceRef); -} - -static bool isPOT(int x) -{ - return (x & (x - 1)) == 0; -} - -- (GLuint)textureName -{ - if (_textureName == 0) - { - TGPaintHasGLError(); - - glGenTextures(1, &_textureName); - glBindTexture(GL_TEXTURE_2D, _textureName); - - bool mipMappable = isPOT(_width) && isPOT(_height); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipMappable ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR); - - glPixelStorei(GL_UNPACK_ALIGNMENT, _unpackAlign); - - glTexImage2D(GL_TEXTURE_2D, 0, _format, _width, _height, 0, _format, _type, _data); - TGPaintHasGLError(); - - if (mipMappable) - { - glGenerateMipmap(GL_TEXTURE_2D); - TGPaintHasGLError(); - } - } - - return _textureName; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintUndoManager.m b/submodules/LegacyComponents/Sources/TGPaintUndoManager.m deleted file mode 100644 index 7fd2a1beb30..00000000000 --- a/submodules/LegacyComponents/Sources/TGPaintUndoManager.m +++ /dev/null @@ -1,158 +0,0 @@ -#import "TGPaintUndoManager.h" - -#import "LegacyComponentsInternal.h" - -#import - -@interface TGPaintUndoOperation : NSObject - -@property (nonatomic, readonly) NSInteger uuid; -@property (nonatomic, readonly) void (^block)(TGPaintUndoManager *); - -- (void)performWithManager:(TGPaintUndoManager *)manager; - -+ (instancetype)operationWithUUID:(NSInteger)uuid block:(void (^)(TGPaintUndoManager *manager))block; - -@end - -@interface TGPaintUndoManager () -{ - SQueue *_queue; - NSMutableArray *_operations; - NSMutableDictionary *_uuidToOperationMap; -} -@end - -@implementation TGPaintUndoManager - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - _queue = [[SQueue alloc] init]; - _operations = [[NSMutableArray alloc] init]; - _uuidToOperationMap = [[NSMutableDictionary alloc] init]; - } - return self; -} - -- (id)copyWithZone:(NSZone *)__unused zone -{ - TGPaintUndoManager *undoManager = [[TGPaintUndoManager alloc] init]; - undoManager->_operations = [[NSMutableArray alloc] initWithArray:_operations copyItems:true]; - undoManager->_uuidToOperationMap = [[NSMutableDictionary alloc] initWithDictionary:_uuidToOperationMap copyItems:true]; - return undoManager; -} - -- (bool)canUndo -{ - return _operations.count > 0; -} - -- (void)undo -{ - [_queue dispatch:^ - { - if (_operations.count == 0) - return; - - NSNumber *key = _operations.lastObject; - TGPaintUndoOperation *operation = _uuidToOperationMap[key]; - [_uuidToOperationMap removeObjectForKey:key]; - [_operations removeLastObject]; - - TGDispatchOnMainThread(^ - { - [operation performWithManager:self]; - - if (self.historyChanged != nil) - self.historyChanged(); - }); - }]; -} - -- (void)registerUndoWithUUID:(NSInteger)uuid block:(void (^)(TGPainting *, TGPhotoEntitiesContainerView *, NSInteger))block -{ - [_queue dispatch:^ - { - TGPaintUndoOperation *operation = [TGPaintUndoOperation operationWithUUID:uuid block:^(TGPaintUndoManager *manager) - { - [manager _performBlock:block uuid:uuid]; - }]; - - NSNumber *key = @(uuid); - _uuidToOperationMap[key] = operation; - [_operations addObject:key]; - - [self _notifyOfHistoryChanges]; - }]; -} - -- (void)unregisterUndoWithUUID:(NSInteger)uuid -{ - [_queue dispatch:^ - { - NSNumber *key = @(uuid); - [_uuidToOperationMap removeObjectForKey:key]; - [_operations removeObject:key]; - - [self _notifyOfHistoryChanges]; - }]; -} - -- (void)_performBlock:(void (^)(TGPainting *, TGPhotoEntitiesContainerView *, NSInteger))block uuid:(NSInteger)uuid -{ - block(self.painting, self.entitiesContainer, uuid); -} - -- (void)reset -{ - [_queue dispatch:^ - { - [_operations removeAllObjects]; - [_uuidToOperationMap removeAllObjects]; - - [self _notifyOfHistoryChanges]; - }]; -} - -- (void)_notifyOfHistoryChanges -{ - TGDispatchOnMainThread(^ - { - if (self.historyChanged != nil) - self.historyChanged(); - }); -} - -@end - - -@implementation TGPaintUndoOperation - -- (id)copyWithZone:(NSZone *)__unused zone -{ - TGPaintUndoOperation *operation = [[TGPaintUndoOperation alloc] init]; - operation->_uuid = _uuid; - operation->_block = [_block copy]; - return operation; -} - -- (void)performWithManager:(TGPaintUndoManager *)manager -{ - self.block(manager); -} - -+ (instancetype)operationWithUUID:(NSInteger)uuid block:(void (^)(TGPaintUndoManager *manager))block -{ - if (uuid == 0 || block == nil) - return nil; - - TGPaintUndoOperation *operation = [[TGPaintUndoOperation alloc] init]; - operation->_uuid = uuid; - operation->_block = [block copy]; - return operation; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPainting.h b/submodules/LegacyComponents/Sources/TGPainting.h deleted file mode 100644 index b2834dc781c..00000000000 --- a/submodules/LegacyComponents/Sources/TGPainting.h +++ /dev/null @@ -1,50 +0,0 @@ -#import -#import -#import -#import -#import - -@class TGPaintBrush; -@class TGPaintShader; -@class TGPaintPath; -@class TGPaintUndoManager; - -@interface TGPainting : NSObject - -@property (nonatomic, readonly) EAGLContext *context; -@property (nonatomic, readonly) GLuint textureName; - -@property (nonatomic, readonly) bool isEmpty; - -@property (nonatomic, readonly) CGSize size; -@property (nonatomic, readonly) CGRect bounds; - -@property (nonatomic, strong) TGPaintBrush *brush; -@property (nonatomic, strong) TGPaintPath *activePath; - -@property (nonatomic, copy) void (^contentChanged)(CGRect rect); -@property (nonatomic, copy) void (^strokeCommited)(void); - -- (instancetype)initWithSize:(CGSize)size undoManager:(TGPaintUndoManager *)undoManager imageData:(NSData *)imageData; - -- (void)performSynchronouslyInContext:(void (^)(void))block; -- (void)performAsynchronouslyInContext:(void (^)(void))block; - -- (void)paintStroke:(TGPaintPath *)path clearBuffer:(bool)clearBuffer completion:(void (^)(void))completion; -- (void)commitStrokeWithColor:(UIColor *)color erase:(bool)erase; - -- (void)renderWithProjection:(GLfloat *)projection; -- (NSData *)imageDataForRect:(CGRect)rect resultPaintingData:(NSData **)resultPaintingData; - -- (UIImage *)imageWithSize:(CGSize)size andData:(NSData *__autoreleasing *)outData; - -- (TGPaintShader *)shaderForKey:(NSString *)key; - -- (void)clear; - -- (GLuint)_quad; -- (GLfloat *)_projection; - -- (dispatch_queue_t)_queue; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPainting.m b/submodules/LegacyComponents/Sources/TGPainting.m deleted file mode 100644 index 3eeb728aafa..00000000000 --- a/submodules/LegacyComponents/Sources/TGPainting.m +++ /dev/null @@ -1,723 +0,0 @@ -#import "TGPainting.h" - -#import "LegacyComponentsInternal.h" - -#import -#import -#import -#import "matrix.h" - -#import "TGPaintBrush.h" -#import "TGPaintPath.h" -#import "TGPaintRender.h" -#import "TGPaintSlice.h" -#import "TGPaintShader.h" -#import "TGPaintShaderSet.h" -#import "TGPaintTexture.h" -#import -#import - -@interface TGPainting () -{ - EAGLContext *_context; - NSDictionary *_shaders; - - NSData *_initialImageData; - - GLuint _textureName; - GLuint _quadVAO; - GLuint _quadVBO; - GLfloat _projection[16]; - - CGRect _activeStrokeBounds; - - GLuint _reusableFramebuffer; - GLuint _paintTextureName; - - TGPaintTexture *_brushTexture; - - TGPaintRenderState *_renderState; - NSInteger _suppressChangesCounter; - - SQueue *_queue; - - __weak TGPaintUndoManager *_undoManager; - NSUInteger _strokeCount; -} -@end - -@implementation TGPainting - -- (instancetype)initWithSize:(CGSize)size undoManager:(TGPaintUndoManager *)undoManager imageData:(NSData *)imageData -{ - self = [super init]; - if (self != nil) - { - _queue = [[SQueue alloc] init]; - _undoManager = undoManager; - - _initialImageData = imageData; - _renderState = [[TGPaintRenderState alloc] init]; - - if (_initialImageData.length > 0) - _strokeCount++; - - [self setSize:size]; - } - return self; -} - -- (void)dealloc -{ - if (_context == nil) - return; - - [self performSynchronouslyInContext:^{ - [EAGLContext setCurrentContext:_context]; - if (_paintTextureName != 0) - glDeleteTextures(1, &_paintTextureName); - - glDeleteBuffers(1, &_quadVBO); - glDeleteVertexArraysOES(1, &_quadVAO); - - if (_reusableFramebuffer != 0) - glDeleteFramebuffers(1, &_reusableFramebuffer); - - if (_textureName != 0) - glDeleteTextures(1, &_textureName); - - [_brushTexture cleanResources]; - - TGPaintHasGLError(); - [EAGLContext setCurrentContext:nil]; - }]; -} - -- (void)setSize:(CGSize)size -{ - _size = size; - mat4f_LoadOrtho(0, (GLint)size.width, 0, (GLint)size.height, -1.0f, 1.0f, _projection); -} - -- (EAGLContext *)context -{ - if (_context == nil) - { - _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; - - if (_context != nil && [EAGLContext setCurrentContext:_context]) - { - glEnable(GL_BLEND); - glDisable(GL_DITHER); - glDisable(GL_STENCIL_TEST); - glDisable(GL_DEPTH_TEST); - } - [self _setupShaders]; - } - - return _context; -} - -- (bool)isEmpty -{ - return (_strokeCount == 0); -} - -- (CGRect)bounds -{ - return CGRectMake(0.0f, 0.0f, self.size.width, self.size.height); -} - -#pragma mark - - -- (void)beginSuppressingChanges -{ - _suppressChangesCounter++; -} - -- (void)endSuppressingChanges -{ - _suppressChangesCounter--; -} - -- (bool)isSuppressingChanges -{ - return _suppressChangesCounter > 0; -} - -#pragma mark - - -- (void)performSynchronouslyInContext:(void (^)(void))block -{ - [_queue dispatch:^ - { - [EAGLContext setCurrentContext:self.context]; - block(); - } synchronous:true]; -} - -- (void)performAsynchronouslyInContext:(void (^)(void))block -{ - [_queue dispatch:^ - { - [EAGLContext setCurrentContext:self.context]; - block(); - }]; -} - -- (void)paintStroke:(TGPaintPath *)path clearBuffer:(bool)clearBuffer completion:(void (^)(void))completion -{ - [self performAsynchronouslyInContext:^ - { - _activePath = path; - - CGRect bounds = CGRectZero; - glBindFramebuffer(GL_FRAMEBUFFER, [self _reusableFramebuffer]); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, [self _paintTextureName], 0); - - GLuint status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - - if (status == GL_FRAMEBUFFER_COMPLETE) - { - glViewport(0, 0, (GLint)self.size.width, (GLint)self.size.height); - - if (clearBuffer) - { - glClearColor(0, 0, 0, 0); - glClear(GL_COLOR_BUFFER_BIT); - } - - [self _setupBrush]; - bounds = [TGPaintRender renderPath:path renderState:_renderState]; - } - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - TGPaintHasGLError(); - - if (self.contentChanged != nil) - self.contentChanged(bounds); - - _activeStrokeBounds = TGPaintUnionRect(_activeStrokeBounds, bounds); - - if (completion != nil) - completion(); - }]; -} - -- (void)commitStrokeWithColor:(UIColor *)color erase:(bool)erase -{ - [self performAsynchronouslyInContext:^ - { - [self registerUndoInRect:_activeStrokeBounds]; - - [self beginSuppressingChanges]; - - [self updateWithBlock:^ - { - GLfloat proj[16]; - mat4f_LoadOrtho(0, (GLint)self.size.width, 0, (GLint)self.size.height, -1.0f, 1.0f, proj); - - TGPaintShader *shader = erase ? [self shaderForKey:@"compositeWithEraseMask"] : [self shaderForKey:@"compositeWithMask"]; - if (_brush.lightSaber) - shader = [self shaderForKey:@"compositeWithMaskLight"]; - glUseProgram(shader.program); - - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, proj); - glUniform1i([shader uniformForKey:@"texture"], 0); - glUniform1i([shader uniformForKey:@"mask"], 1); - if (!erase) - TGSetupColorUniform([shader uniformForKey:@"color"], color); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, self.textureName); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, self._paintTextureName); - - glBlendFunc(GL_ONE, GL_ZERO); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindVertexArrayOES(0); - - glBindTexture(GL_TEXTURE_2D, self.textureName); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - } bounds:_activeStrokeBounds]; - - [self endSuppressingChanges]; - - [_renderState reset]; - - _activeStrokeBounds = CGRectZero; - - _activePath = nil; - - TGDispatchOnMainThread(^ - { - if (self.strokeCommited != nil) - self.strokeCommited(); - }); - }]; -} - -- (void)setBrush:(TGPaintBrush *)brush -{ - _brush = brush; - [self performAsynchronouslyInContext:^{ - if (_brushTexture != nil) - { -// [_brushTexture cleanResources]; - _brushTexture = nil; - } - }]; -} - -- (void)_setupBrush -{ - TGPaintShader *shader = [self shaderForKey:_brush.lightSaber ? @"brushLight" : @"brush"]; - glUseProgram(shader.program); - - glActiveTexture(GL_TEXTURE0); - - if (_brushTexture == nil) - _brushTexture = [[TGPaintTexture alloc] initWithCGImage:_brush.stampRef forceRGB:false]; - - glBindTexture(GL_TEXTURE_2D, _brushTexture.textureName); - - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, _projection); - glUniform1i([shader uniformForKey:@"texture"], 0); - TGPaintHasGLError(); -} - -#pragma mark - - -- (void)updateWithBlock:(void (^)(void))updateBlock bounds:(CGRect)bounds -{ - glBindFramebuffer(GL_FRAMEBUFFER, [self _reusableFramebuffer]); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.textureName, 0); - - GLuint status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (status == GL_FRAMEBUFFER_COMPLETE) - { - glViewport(0, 0, (GLint)self.size.width, (GLint)self.size.height); - - TGPaintHasGLError(); - updateBlock(); - TGPaintHasGLError(); - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - if (![self isSuppressingChanges]) - { - if (self.contentChanged != nil) - self.contentChanged(bounds); - } -} - -- (void)clear -{ - _strokeCount = 0; - - [self performAsynchronouslyInContext:^ - { - [self updateWithBlock:^ - { - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - } bounds:[self bounds]]; - }]; -} - -- (dispatch_queue_t)_queue -{ - return _queue._dispatch_queue; -} - -#pragma mark - - -- (void)renderWithProjection:(GLfloat *)projection -{ - if (_activePath != nil) - { - if (_activePath.action == TGPaintActionErase) - [self _renderWithProjection:projection mask:[self _paintTextureName] color:nil erase:true]; - else - [self _renderWithProjection:projection mask:[self _paintTextureName] color:_activePath.color erase:false]; - } - else - { - [self _renderWithProjection:projection]; - } -} - -- (void)_renderWithProjection:(GLfloat *)projection mask:(GLint)mask color:(UIColor *)color erase:(bool)erase -{ - TGPaintShader *shader = erase ? [self shaderForKey:@"blitWithEraseMask"] : [self shaderForKey:@"blitWithMask"]; - if (_brush.lightSaber) - shader = [self shaderForKey:@"blitWithMaskLight"]; - - glUseProgram(shader.program); - - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, projection); - glUniform1i([shader uniformForKey:@"texture"], 0); - glUniform1i([shader uniformForKey:@"mask"], 1); - if (!erase) - TGSetupColorUniform([shader uniformForKey:@"color"], color); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, self.textureName); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, mask); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindVertexArrayOES(0); -} - -- (void)_renderWithProjection:(GLfloat *)projection -{ - TGPaintShader *shader = [self shaderForKey:@"blit"]; - glUseProgram(shader.program); - - glUniformMatrix4fv([shader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, projection); - glUniform1i([shader uniformForKey:@"texture"], 0); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, self.textureName); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindVertexArrayOES(0); -} - -#pragma mark - - -- (NSData *)imageDataForRect:(CGRect)rect resultPaintingData:(NSData **)resultPaintingData -{ - [EAGLContext setCurrentContext:self.context]; - - TGPaintHasGLError(); - - GLint minX = (GLint) CGRectGetMinX(rect); - GLint minY = (GLint) CGRectGetMinY(rect); - GLint width = (GLint) CGRectGetWidth(rect); - GLint height = (GLint) CGRectGetHeight(rect); - - GLuint framebuffer; - glGenFramebuffers(1, &framebuffer); - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - - GLuint colorRenderbuffer; - glGenRenderbuffers(1, &colorRenderbuffer); - glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8_OES, width, height); - - GLuint textureName; - GLenum format = GL_RGBA; - GLenum type = GL_UNSIGNED_BYTE; - - glGenTextures(1, &textureName); - glBindTexture(GL_TEXTURE_2D, textureName); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - glBindTexture(GL_TEXTURE_2D, textureName); - - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureName, 0); - - GLint status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - - if (status != GL_FRAMEBUFFER_COMPLETE) - { - TGLegacyLog(@"ERROR: imageAndData: - Incomplete Framebuffer!"); - TGPaintHasGLError(); - return nil; - } - - glViewport(0, 0, (GLint)self.size.width, (GLint)self.size.height); - - TGPaintShader *blitShader = [self shaderForKey:@"nonPremultipliedBlit"]; - glUseProgram(blitShader.program); - - GLfloat proj[16], effectiveProj[16],final[16]; - mat4f_LoadOrtho(0, (GLint)self.size.width, 0, (GLint)self.size.height, -1.0f, 1.0f, proj); - - CGAffineTransform translate = CGAffineTransformMakeTranslation(-minX, -minY); - mat4f_LoadCGAffineTransform(effectiveProj, translate); - mat4f_MultiplyMat4f(proj, effectiveProj, final); - - glUniformMatrix4fv([blitShader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, final); - glUniform1i([blitShader uniformForKey:@"texture"], (GLuint)0); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, self.textureName); - - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArrayOES(0); - - NSUInteger length = width * 4 * height; - GLubyte *pixels = malloc(sizeof(GLubyte) * length); - glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - NSData *paintingResult = [NSData dataWithBytes:pixels length:length]; - - if (resultPaintingData != NULL) - *resultPaintingData = paintingResult; - - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer); - - status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - - if (status != GL_FRAMEBUFFER_COMPLETE) - { - TGLegacyLog(@"ERROR: imageAndData: - Incomplete Framebuffer!"); - TGPaintHasGLError(); - return nil; - } - - blitShader = [self shaderForKey:@"blit"]; - glUseProgram(blitShader.program); - - glUniformMatrix4fv([blitShader uniformForKey:@"mvpMatrix"], 1, GL_FALSE, final); - glUniform1i([blitShader uniformForKey:@"texture"], (GLuint)0); - - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textureName); - - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glClear(GL_COLOR_BUFFER_BIT); - - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArrayOES([self _quad]); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArrayOES(0); - - glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - NSData *result = [NSData dataWithBytes:pixels length:length]; - free(pixels); - - glDeleteFramebuffers(1, &framebuffer); - glDeleteTextures(1, &textureName); - glDeleteRenderbuffers(1, &colorRenderbuffer); - - TGPaintHasGLError(); - return result; -} - -- (UIImage *)imageWithSize:(CGSize)size andData:(NSData *__autoreleasing *)outData -{ - NSData *paintingData = nil; - NSData *imageData = [self imageDataForRect:self.bounds resultPaintingData:&paintingData]; - UIImage *image = [self imageForData:imageData size:self.size outputSize:size]; - - if (outData != NULL) - *outData = paintingData; - - return image; -} - -- (UIImage *)imageForData:(NSData *)data size:(CGSize)size outputSize:(CGSize)outputSize -{ - size_t width = (size_t)size.width; - size_t height = (size_t)size.height; - - CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate((void *)data.bytes, width, height, 8, width * 4, colorSpaceRef, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast); - - CGImageRef imageRef = CGBitmapContextCreateImage(context); - CGContextRelease(context); - CGColorSpaceRelease(colorSpaceRef); - - UIGraphicsBeginImageContext(outputSize); - [[UIImage imageWithCGImage:imageRef] drawInRect:CGRectMake(0.0f, 0.0f, outputSize.width, outputSize.height)]; - CGImageRelease(imageRef); - - UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return result; -} - -#pragma mark - - -- (void)registerUndoInRect:(CGRect)rect -{ - rect = CGRectIntersection(rect, self.bounds); - - NSData *paintingData = nil; - [self imageDataForRect:rect resultPaintingData:&paintingData]; - - TGPaintSlice *slice = [[TGPaintSlice alloc] initWithData:paintingData bounds:rect]; - NSInteger uuid; - arc4random_buf(&uuid, sizeof(NSInteger)); - [_undoManager registerUndoWithUUID:uuid block:^(TGPainting *painting, __unused TGPhotoEntitiesContainerView *entitiesContainer, __unused NSInteger uuid) - { - [painting restoreSlice:slice redo:false]; - }]; - - _strokeCount++; -} - -- (void)restoreSlice:(TGPaintSlice *)slice redo:(bool)redo -{ - [self performAsynchronouslyInContext:^ - { - if (!redo) - _strokeCount--; - - NSData *data = slice.data; - - glBindTexture(GL_TEXTURE_2D, self.textureName); - glTexSubImage2D(GL_TEXTURE_2D, 0, (GLint)slice.bounds.origin.x, (GLint)slice.bounds.origin.y, (GLint)slice.bounds.size.width, (GLint)slice.bounds.size.height, GL_RGBA, GL_UNSIGNED_BYTE, data.bytes); - - if (![self isSuppressingChanges] && self.contentChanged != nil) - self.contentChanged(slice.bounds); - }]; -} - -#pragma mark - - -- (GLuint)textureName -{ - if (_textureName == 0) - { - _textureName = [self _generateTextureWithPixels:(GLubyte *)_initialImageData.bytes]; - _initialImageData = nil; - } - - return _textureName; -} - -- (GLuint)_paintTextureName -{ - if (_paintTextureName == 0) - _paintTextureName = [self _generateTextureWithPixels:nil]; - - return _paintTextureName; -} - -- (GLuint)_reusableFramebuffer -{ - if (_reusableFramebuffer == 0) - glGenFramebuffers(1, &_reusableFramebuffer); - - return _reusableFramebuffer; -} - -- (GLuint)_quad -{ - if (_quadVAO == 0) - { - [EAGLContext setCurrentContext:self.context]; - CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); - - CGPoint corners[4]; - corners[0] = rect.origin; - corners[1] = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)); - corners[2] = CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)); - corners[3] = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)); - - const GLfloat vertices[] = - { - (GLfloat)corners[0].x, (GLfloat)corners[0].y, 0.0, 0.0, - (GLfloat)corners[1].x, (GLfloat)corners[1].y, 1.0, 0.0, - (GLfloat)corners[3].x, (GLfloat)corners[3].y, 0.0, 1.0, - (GLfloat)corners[2].x, (GLfloat)corners[2].y, 1.0, 1.0, - }; - - glGenVertexArraysOES(1, &_quadVAO); - glBindVertexArrayOES(_quadVAO); - - glGenBuffers(1, &_quadVBO); - glBindBuffer(GL_ARRAY_BUFFER, _quadVBO); - glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 16, vertices, GL_STATIC_DRAW); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void*)8); - glEnableVertexAttribArray(1); - - glBindBuffer(GL_ARRAY_BUFFER,0); - glBindVertexArrayOES(0); - } - - return _quadVAO; -} - -- (GLfloat *)_projection -{ - return _projection; -} - -- (GLuint)_generateTextureWithPixels:(GLubyte *)pixels -{ - [EAGLContext setCurrentContext:self.context]; - TGPaintHasGLError(); - - GLuint textureName; - glGenTextures(1, &textureName); - glBindTexture(GL_TEXTURE_2D, textureName); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - GLuint width = (GLuint)self.size.width; - GLuint height = (GLuint)self.size.height; - GLenum format = GL_RGBA; - GLenum type = GL_UNSIGNED_BYTE; - NSUInteger bytesPerPixel = 4; - - if (pixels == NULL) - { - pixels = calloc((size_t) (self.size.width * bytesPerPixel * self.size.height), sizeof(GLubyte)); - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, pixels); - free(pixels); - } - else - { - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, pixels); - } - - TGPaintHasGLError(); - return textureName; -} - -#pragma mark - Shaders - -- (void)_setupShaders -{ - if (_shaders != nil) - return; - - _shaders = [TGPaintShaderSet setup]; -} - -- (TGPaintShader *)shaderForKey:(NSString *)key -{ - return _shaders[key]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPaintingData.m b/submodules/LegacyComponents/Sources/TGPaintingData.m index def8a898275..59a89746c59 100644 --- a/submodules/LegacyComponents/Sources/TGPaintingData.m +++ b/submodules/LegacyComponents/Sources/TGPaintingData.m @@ -6,13 +6,11 @@ #import "TGPhotoPaintStickerEntity.h" #import "TGMediaEditingContext.h" -#import "TGPaintUndoManager.h" @interface TGPaintingData () { UIImage *_image; UIImage *_stillImage; - NSData *_data; UIImage *(^_imageRetrievalBlock)(void); UIImage *(^_stillImageRetrievalBlock)(void); @@ -21,21 +19,24 @@ @interface TGPaintingData () @implementation TGPaintingData -+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager ++ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers { TGPaintingData *paintingData = [[TGPaintingData alloc] init]; - paintingData->_data = data; + paintingData->_drawingData = data; paintingData->_image = image; paintingData->_stillImage = stillImage; - paintingData->_entities = entities; - paintingData->_undoManager = undoManager; + paintingData->_entitiesData = entitiesData; + paintingData->_hasAnimation = hasAnimation; + paintingData->_stickers = stickers; return paintingData; } -+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities { ++ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers { TGPaintingData *paintingData = [[TGPaintingData alloc] init]; paintingData->_imagePath = imagePath; - paintingData->_entities = entities; + paintingData->_entitiesData = entitiesData; + paintingData->_hasAnimation = hasAnimation; + paintingData->_stickers = stickers; return paintingData; } @@ -49,7 +50,9 @@ + (instancetype)dataWithPaintingImagePath:(NSString *)imagePath - (instancetype)dataForAnimation { TGPaintingData *paintingData = [[TGPaintingData alloc] init]; - paintingData->_entities = _entities; + paintingData->_entitiesData = _entitiesData; + paintingData->_hasAnimation = _hasAnimation; + paintingData->_stickers = _stickers; return paintingData; } @@ -58,17 +61,17 @@ + (void)storePaintingData:(TGPaintingData *)data inContext:(TGMediaEditingContex [[TGPaintingData queue] dispatch:^ { NSURL *dataUrl = nil; + NSURL *entitiesDataUrl = nil; NSURL *imageUrl = nil; - NSData *compressedData = TGPaintGZipDeflate(data.data); - [context setPaintingData:compressedData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl imageUrl:&imageUrl forVideo:video]; + NSData *compressedDrawingData = TGPaintGZipDeflate(data.drawingData); + NSData *compressedEntitiesData = TGPaintGZipDeflate(data.entitiesData); + [context setPaintingData:compressedDrawingData entitiesData:compressedEntitiesData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl entitiesDataUrl:&entitiesDataUrl imageUrl:&imageUrl forVideo:video]; __weak TGMediaEditingContext *weakContext = context; [[SQueue mainQueue] dispatch:^ { - data->_dataPath = dataUrl.path; data->_imagePath = imageUrl.path; - data->_data = nil; data->_imageRetrievalBlock = ^UIImage * { @@ -100,20 +103,6 @@ + (void)facilitatePaintingData:(TGPaintingData *)data }]; } -- (void)dealloc -{ - [self.undoManager reset]; -} - -- (NSData *)data -{ - if (_data != nil) - return _data; - else if (_dataPath != nil) - return TGPaintGZipInflate([[NSData alloc] initWithContentsOfFile:_dataPath]); - else - return nil; -} - (UIImage *)image { @@ -128,34 +117,13 @@ - (UIImage *)image - (UIImage *)stillImage { if (_stillImage != nil) - return _stillImage; + return _stillImage; else if (_stillImageRetrievalBlock != nil) return _stillImageRetrievalBlock(); else return nil; } -- (NSArray *)stickers -{ - NSMutableSet *stickers = [[NSMutableSet alloc] init]; - for (TGPhotoPaintEntity *entity in self.entities) - { - if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) - [stickers addObject:((TGPhotoPaintStickerEntity *)entity).document]; - } - return [stickers allObjects]; -} - -- (bool)hasAnimation -{ - for (TGPhotoPaintEntity *entity in self.entities) - { - if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]] && ((TGPhotoPaintStickerEntity *)entity).animated) - return true; - } - return false; -} - - (BOOL)isEqual:(id)object { if (object == self) @@ -165,7 +133,7 @@ - (BOOL)isEqual:(id)object return false; TGPaintingData *data = (TGPaintingData *)object; - return [data.entities isEqual:self.entities] && ((data.data != nil && [data.data isEqualToData:self.data]) || (data.data == nil && self.data == nil)); + return [data.entitiesData isEqual:self.entitiesData] && ((data.drawingData != nil && [data.drawingData isEqualToData:self.drawingData]) || (data.drawingData == nil && self.drawingData == nil)); } + (SQueue *)queue diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropView.m b/submodules/LegacyComponents/Sources/TGPhotoAvatarCropView.m index ddd3a11fa4f..521eb6c6c5a 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarCropView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarCropView.m @@ -9,7 +9,6 @@ #import "TGPhotoEditorInterfaceAssets.h" #import "PGPhotoEditorView.h" -#import "TGPhotoEntitiesContainerView.h" const CGFloat TGPhotoAvatarCropViewOverscreenSize = 1000; const CGFloat TGPhotoAvatarCropViewCurtainSize = 300; @@ -46,13 +45,13 @@ @interface TGPhotoAvatarCropView () __weak PGPhotoEditorView *_fullPreviewView; __weak UIImageView *_fullPaintingView; - __weak TGPhotoEntitiesContainerView *_fullEntitiesView; + __weak UIView *_fullEntitiesView; } @end @implementation TGPhotoAvatarCropView -- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square +- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView *)fullEntitiesView square:(bool)square { self = [super initWithFrame:CGRectZero]; if (self != nil) diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h index d9ed3bee578..d702900441e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.h @@ -3,7 +3,6 @@ @class PGPhotoEditor; @class PGPhotoTool; @class TGPhotoEditorPreviewView; -@class TGPhotoEntitiesContainerView; @class PGPhotoEditorView; @class TGMediaPickerGalleryVideoScrubber; @@ -13,6 +12,9 @@ @property (nonatomic, assign) bool skipTransitionIn; @property (nonatomic, assign) bool fromCamera; +@property (nonatomic, copy) void (^cancelPressed)(void); +@property (nonatomic, copy) void (^donePressed)(void); + @property (nonatomic, copy) void (^croppingChanged)(void); @property (nonatomic, copy) void (^togglePlayback)(void); @@ -20,10 +22,12 @@ @property (nonatomic, weak) UIView *dotMarkerView; @property (nonatomic, weak) PGPhotoEditorView *fullPreviewView; @property (nonatomic, weak) UIImageView *fullPaintingView; -@property (nonatomic, weak) TGPhotoEntitiesContainerView *fullEntitiesView; +@property (nonatomic, weak) UIView *fullEntitiesView; @property (nonatomic, weak) TGMediaPickerGalleryVideoScrubber *scrubberView; -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum; +@property (nonatomic, strong) id stickersContext; + +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion isSuggesting:(bool)isSuggesting senderName:(NSString *)senderName; - (void)setImage:(UIImage *)image; - (void)setSnapshotImage:(UIImage *)snapshotImage; diff --git a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m index a3a92bd16c9..183cd5a500a 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoAvatarPreviewController.m @@ -16,9 +16,11 @@ #import "TGMediaPickerGalleryVideoScrubber.h" #import "TGModernGalleryVideoView.h" -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoPaintController.h" + +#import "TGPhotoPaintStickersContext.h" + +#import "TGPhotoDrawingController.h" const CGFloat TGPhotoAvatarPreviewPanelSize = 96.0f; const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanelSize + 40.0f; @@ -34,7 +36,7 @@ @interface TGPhotoAvatarPreviewController () UIView *_wrapperView; __weak TGPhotoAvatarCropView *_cropView; - + UIView *_portraitToolsWrapperView; UIView *_landscapeToolsWrapperView; UIView *_portraitWrapperBackgroundView; @@ -44,11 +46,19 @@ @interface TGPhotoAvatarPreviewController () UIView *_landscapeToolControlView; UILabel *_coverLabel; + TGModernButton *_cancelButton; + UILabel *_titleLabel; + UILabel *_subtitleLabel; + UIView *_doneButton; + bool _wasPlayingBeforeCropping; bool _scheduledTransitionIn; bool _isForum; + bool _isSuggestion; + bool _isSuggesting; + NSString *_senderName; } @property (nonatomic, weak) PGPhotoEditor *photoEditor; @@ -58,13 +68,16 @@ @interface TGPhotoAvatarPreviewController () @implementation TGPhotoAvatarPreviewController -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum { +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion isSuggesting:(bool)isSuggesting senderName:(NSString *)senderName { self = [super initWithContext:context]; if (self != nil) { self.photoEditor = photoEditor; self.previewView = previewView; _isForum = isForum; + _isSuggestion = isSuggestion; + _isSuggesting = isSuggesting; + _senderName = senderName; } return self; } @@ -173,12 +186,65 @@ - (void)loadView _coverLabel.backgroundColor = [UIColor clearColor]; _coverLabel.font = TGSystemFontOfSize(14.0f); _coverLabel.textColor = [UIColor whiteColor]; - _coverLabel.text = TGLocalized(@"PhotoEditor.SelectCoverFrame"); + _coverLabel.text = _isSuggesting ? TGLocalized(@"PhotoEditor.SelectCoverFrameSuggestion") : TGLocalized(@"PhotoEditor.SelectCoverFrame"); [_coverLabel sizeToFit]; [_portraitToolsWrapperView addSubview:_coverLabel]; [_wrapperView addSubview:_dotImageView]; } + + if (_isSuggestion) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.backgroundColor = [UIColor clearColor]; + _titleLabel.font = TGBoldSystemFontOfSize(17.0f); + _titleLabel.textColor = [UIColor whiteColor]; + _titleLabel.text = self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoTitle") : TGLocalized(@"Conversation.SuggestedPhotoTitle"); + [_titleLabel sizeToFit]; + [_wrapperView addSubview:_titleLabel]; + + NSMutableAttributedString *subtitle = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoTextExpanded") : TGLocalized(@"Conversation.SuggestedPhotoTextExpanded"), _senderName]]; + [subtitle addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, subtitle.string.length)]; + [subtitle addAttribute:NSFontAttributeName value:TGSystemFontOfSize(15.0f) range:NSMakeRange(0, subtitle.string.length)]; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = NSTextAlignmentCenter; + paragraphStyle.lineSpacing = 5.0f; + [subtitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, subtitle.string.length)]; + + _subtitleLabel = [[UILabel alloc] init]; + _subtitleLabel.attributedText = subtitle; + _subtitleLabel.backgroundColor = [UIColor clearColor]; + _subtitleLabel.numberOfLines = 2; + if (!self.item.isVideo) { + [_wrapperView addSubview:_subtitleLabel]; + } + + if (!self.item.isVideo) { + _cancelButton = [[TGModernButton alloc] init]; + [_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal]; + _cancelButton.titleLabel.font = TGSystemFontOfSize(17.0); + [_cancelButton sizeToFit]; + [_cancelButton addTarget:self action:@selector(cancelButtonPressed) forControlEvents:UIControlEventTouchUpInside]; + [_wrapperView addSubview:_cancelButton]; + + if (_stickersContext != nil) { + _doneButton = [_stickersContext solidRoundedButton:self.item.isVideo ? TGLocalized(@"PhotoEditor.SetAsMyVideo") : TGLocalized(@"PhotoEditor.SetAsMyPhoto") action:^{ + __strong TGPhotoAvatarPreviewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf.donePressed != nil) + strongSelf.donePressed(); + }]; + [_wrapperView addSubview:_doneButton]; + } + } + } +} + +- (void)cancelButtonPressed { + if (self.cancelPressed != nil) + self.cancelPressed(); } - (void)viewWillAppear:(BOOL)animated @@ -325,8 +391,18 @@ - (void)transitionIn [_cropView animateTransitionIn]; + _cancelButton.alpha = 0.0f; + _titleLabel.alpha = 0.0f; + _subtitleLabel.alpha = 0.0f; + _doneButton.alpha = 0.0f; + [UIView animateWithDuration:0.3f animations:^ { + _cancelButton.alpha = 1.0f; + _titleLabel.alpha = 1.0f; + _subtitleLabel.alpha = 1.0f; + _doneButton.alpha = 1.0f; + _portraitToolsWrapperView.alpha = 1.0f; _landscapeToolsWrapperView.alpha = 1.0f; _dotImageView.alpha = 1.0f; @@ -416,7 +492,7 @@ - (void)transitionOutSwitching:(bool)switching completion:(void (^)(void))comple if (self.switchingToTab == TGPhotoEditorPaintTab) { - containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; } CGSize fittedSize = TGScaleToSize(cropRectFrame.size, containerFrame.size); @@ -499,6 +575,10 @@ - (void)transitionOutSwitching:(bool)switching completion:(void (^)(void))comple _landscapeToolsWrapperView.alpha = 0.0f; _dotImageView.alpha = 0.0f; _dotMarkerView.alpha = 0.0f; + _cancelButton.alpha = 0.0f; + _titleLabel.alpha = 0.0f; + _subtitleLabel.alpha = 0.0f; + _doneButton.alpha = 0.0f; } completion:^(__unused BOOL finished) { if (!switching) { @@ -614,6 +694,10 @@ - (void)prepareForCustomTransitionOut _portraitToolsWrapperView.alpha = 0.0f; _landscapeToolsWrapperView.alpha = 0.0f; _dotImageView.alpha = 0.0f; + _titleLabel.alpha = 0.0f; + _subtitleLabel.alpha = 0.0f; + _cancelButton.alpha = 0.0f; + _doneButton.alpha = 0.0f; } completion:nil]; } @@ -742,6 +826,11 @@ - (void)updateToolViews screenEdges.left += safeAreaInset.left; screenEdges.bottom -= safeAreaInset.bottom; screenEdges.right -= safeAreaInset.right; + + CGSize buttonSize = CGSizeMake(MIN(referenceSize.width, referenceSize.height) - 16.0 * 2.0, 50.0f); + [_doneButton updateWidth:buttonSize.width]; + + CGSize subtitleSize = [_subtitleLabel sizeThatFits:CGSizeMake(referenceSize.width - 96.0, referenceSize.height)]; switch (orientation) { @@ -757,6 +846,13 @@ - (void)updateToolViews _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height); + _subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height); + + _cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height); } break; @@ -768,11 +864,17 @@ - (void)updateToolViews }]; _landscapeToolsWrapperView.frame = CGRectMake(screenEdges.right - panelToolbarLandscapeSize, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height); - _portraitToolsWrapperView.frame = CGRectMake(screenEdges.top, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height); + _subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height); + + _cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(floor((_wrapperView.frame.size.width - buttonSize.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, buttonSize.width, buttonSize.height); } break; @@ -793,6 +895,13 @@ - (void)updateToolViews _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.bottom - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); _coverLabel.frame = CGRectMake(floor((_portraitToolsWrapperView.frame.size.width - _coverLabel.frame.size.width) / 2.0), CGRectGetMaxY(_scrubberView.frame) + 6.0, _coverLabel.frame.size.width, _coverLabel.frame.size.height); + + _titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), screenEdges.top + floor((44.0 - _titleLabel.frame.size.height) / 2.0), _titleLabel.frame.size.width, _titleLabel.frame.size.height); + _subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - subtitleSize.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height - subtitleSize.height - 20.0, subtitleSize.width, subtitleSize.height); + + _cancelButton.frame = CGRectMake(screenEdges.left + 16.0, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height); + + _doneButton.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - buttonSize.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height, buttonSize.width, buttonSize.height); } break; } diff --git a/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.h b/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.h deleted file mode 100644 index 1db7d529712..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.h +++ /dev/null @@ -1,15 +0,0 @@ -#import -#import "TGPhotoPaintSettingsView.h" - -@class TGPaintBrush; -@class TGPaintBrushPreview; - -@interface TGPhotoBrushSettingsView : UIView - -@property (nonatomic, copy) void (^brushChanged)(TGPaintBrush *brush); - -@property (nonatomic, strong) TGPaintBrush *brush; - -- (instancetype)initWithBrushes:(NSArray *)brushes preview:(TGPaintBrushPreview *)preview; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.m b/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.m deleted file mode 100644 index 144cf5b68a5..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoBrushSettingsView.m +++ /dev/null @@ -1,205 +0,0 @@ -#import "TGPhotoBrushSettingsView.h" - -#import "LegacyComponentsInternal.h" -#import "TGImageUtils.h" - -#import "TGPhotoEditorSliderView.h" - -#import - -#import "TGPaintBrush.h" -#import "TGPaintBrushPreview.h" - -const CGFloat TGPhotoBrushSettingsViewMargin = 10.0f; -const CGFloat TGPhotoBrushSettingsItemHeight = 44.0f; - -@interface TGPhotoBrushSettingsView () -{ - NSArray *_brushes; - - UIView *_wrapperView; - UIView *_contentView; - UIVisualEffectView *_effectView; - - NSArray *_brushViews; - NSArray *_brushIconViews; - NSArray *_brushSeparatorViews; -} -@end - -@implementation TGPhotoBrushSettingsView - -@synthesize interfaceOrientation = _interfaceOrientation; - -- (instancetype)initWithBrushes:(NSArray *)brushes preview:(TGPaintBrushPreview *)preview -{ - self = [super initWithFrame:CGRectZero]; - if (self != nil) - { - _brushes = brushes; - - _interfaceOrientation = UIInterfaceOrientationPortrait; - - _wrapperView = [[UIView alloc] init]; - _wrapperView.clipsToBounds = true; - _wrapperView.layer.cornerRadius = 12.0; - [self addSubview:_wrapperView]; - - _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]]; - _effectView.alpha = 0.0f; - _effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [_wrapperView addSubview:_effectView]; - - _contentView = [[UIView alloc] init]; - _contentView.alpha = 0.0f; - _contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [_wrapperView addSubview:_contentView]; - - UIFont *font = [UIFont systemFontOfSize:17]; - - NSMutableArray *brushViews = [[NSMutableArray alloc] init]; - NSMutableArray *brushIconViews = [[NSMutableArray alloc] init]; - NSMutableArray *separatorViews = [[NSMutableArray alloc] init]; - [brushes enumerateObjectsUsingBlock:^(__unused TGPaintBrush *brush, NSUInteger index, __unused BOOL *stop) - { - NSString *title; - UIImage *icon; - switch (index) { - case 0: - title = TGLocalized(@"Paint.Pen"); - icon = [UIImage imageNamed:@"Editor/BrushPen"]; - break; - case 1: - title = TGLocalized(@"Paint.Marker"); - icon = [UIImage imageNamed:@"Editor/BrushMarker"]; - break; - case 2: - title = TGLocalized(@"Paint.Neon"); - icon = [UIImage imageNamed:@"Editor/BrushNeon"]; - break; - case 3: - title = TGLocalized(@"Paint.Arrow"); - icon = [UIImage imageNamed:@"Editor/BrushArrow"]; - break; - default: - break; - } - - TGModernButton *button = [[TGModernButton alloc] initWithFrame:CGRectMake(0, index * TGPhotoBrushSettingsItemHeight, 0, 0)]; - button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - button.titleLabel.font = font; - button.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 16.0f, 0.0f, 0.0f); - button.tag = index; - [button setTitle:title forState:UIControlStateNormal]; - [button setTitleColor:[UIColor whiteColor]]; - [button addTarget:self action:@selector(brushButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - [_contentView addSubview:button]; - [brushViews addObject:button]; - - UIImageView *iconView = [[UIImageView alloc] initWithImage:TGTintedImage(icon, [UIColor whiteColor])]; - [button addSubview:iconView]; - [brushIconViews addObject:iconView]; - - if (index != brushes.count - 1) - { - UIView *separatorView = [[UIView alloc] init]; - separatorView.backgroundColor = UIColorRGBA(0xffffff, 0.2); - [_contentView addSubview:separatorView]; - - [separatorViews addObject:separatorView]; - } - }]; - - _brushViews = brushViews; - _brushIconViews = brushIconViews; - _brushSeparatorViews = separatorViews; - } - return self; -} - -- (void)brushButtonPressed:(TGModernButton *)sender -{ - if (self.brushChanged != nil) - self.brushChanged(_brushes[sender.tag]); -} - -- (void)present -{ - [UIView animateWithDuration:0.25 animations:^ - { - _effectView.alpha = 1.0f; - _contentView.alpha = 1.0f; - } completion:^(__unused BOOL finished) - { - - }]; -} - -- (void)dismissWithCompletion:(void (^)(void))completion -{ - [UIView animateWithDuration:0.2 animations:^ - { - _effectView.alpha = 0.0f; - _contentView.alpha = 0.0f; - } completion:^(__unused BOOL finished) - { - if (completion != nil) - completion(); - }]; -} - -- (CGSize)sizeThatFits:(CGSize)__unused size -{ - return CGSizeMake(220, _brushViews.count * TGPhotoBrushSettingsItemHeight + TGPhotoBrushSettingsViewMargin * 2); -} - -- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - _interfaceOrientation = interfaceOrientation; - - [self setNeedsLayout]; -} - -- (void)layoutSubviews -{ - CGFloat arrowSize = 0.0f; - switch (self.interfaceOrientation) - { - case UIInterfaceOrientationLandscapeLeft: - { - _wrapperView.frame = CGRectMake(TGPhotoBrushSettingsViewMargin - arrowSize, TGPhotoBrushSettingsViewMargin, self.frame.size.width - TGPhotoBrushSettingsViewMargin * 2 + arrowSize, self.frame.size.height - TGPhotoBrushSettingsViewMargin * 2); - } - break; - - case UIInterfaceOrientationLandscapeRight: - { - _wrapperView.frame = CGRectMake(TGPhotoBrushSettingsViewMargin, TGPhotoBrushSettingsViewMargin, self.frame.size.width - TGPhotoBrushSettingsViewMargin * 2 + arrowSize, self.frame.size.height - TGPhotoBrushSettingsViewMargin * 2); - } - break; - - default: - { - _wrapperView.frame = CGRectMake(TGPhotoBrushSettingsViewMargin, TGPhotoBrushSettingsViewMargin, self.frame.size.width - TGPhotoBrushSettingsViewMargin * 2, self.frame.size.height - TGPhotoBrushSettingsViewMargin * 2 + arrowSize); - } - break; - } - - CGFloat thickness = TGScreenPixel; - - [_brushViews enumerateObjectsUsingBlock:^(TGModernButton *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(0.0f, TGPhotoBrushSettingsItemHeight * index, _contentView.frame.size.width, TGPhotoBrushSettingsItemHeight); - }]; - - [_brushIconViews enumerateObjectsUsingBlock:^(UIImageView *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(_contentView.frame.size.width - 42.0f, (TGPhotoBrushSettingsItemHeight - view.frame.size.height) / 2.0, view.frame.size.width, view.frame.size.height); - }]; - - [_brushSeparatorViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(0.0f, TGPhotoBrushSettingsItemHeight * (index + 1), _contentView.frame.size.width, thickness); - }]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index d87e86069e7..d9cd29dbd7b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -50,7 +50,7 @@ - (void)createInputPanelIfNeeded UIView *parentView = [self _parentView]; id inputPanel = nil; - if (_stickersContext) { + if (_stickersContext && _stickersContext.captionPanelView != nil) { inputPanel = _stickersContext.captionPanelView(); } _inputPanel = inputPanel; @@ -241,11 +241,11 @@ - (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets if (animated) { [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ _inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight + 1.0); + _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight); } completion:nil]; } else { _inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight + 1.0); + _backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight); } } diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.h b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.h similarity index 74% rename from submodules/LegacyComponents/Sources/TGPhotoPaintController.h rename to submodules/LegacyComponents/Sources/TGPhotoDrawingController.h index 7cc96e30a06..47b16953643 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.h @@ -7,11 +7,12 @@ @protocol TGPhotoPaintStickersContext; -@interface TGPhotoPaintController : TGPhotoEditorTabController +@interface TGPhotoDrawingController : TGPhotoEditorTabController -@property (nonatomic, strong) id stickersContext; +@property (nonatomic, copy) void (^requestDismiss)(void); +@property (nonatomic, copy) void (^requestApply)(void); -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView; +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView *)entitiesView stickersContext:(id)stickersContext isAvatar:(bool)isAvatar; - (TGPaintingData *)paintingData; diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m new file mode 100644 index 00000000000..a74d4d23166 --- /dev/null +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m @@ -0,0 +1,939 @@ +#import "TGPhotoDrawingController.h" + +#import "LegacyComponentsInternal.h" + +#import + +#import +#import +#import +#import "TGPhotoEditorInterfaceAssets.h" +#import + +#import +#import + +#import +#import + +#import + +#import "TGPaintingWrapperView.h" +#import "TGPhotoEditorSparseView.h" + +#import "PGPhotoEditor.h" +#import "TGPhotoEditorPreviewView.h" + +#import + +const CGFloat TGPhotoPaintTopPanelSize = 44.0f; +const CGFloat TGPhotoPaintBottomPanelSize = 79.0f; +const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f }; +const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f }; + +@interface TGPhotoDrawingController () +{ + id _context; + id _stickersContext; + id _drawingAdapter; + + TGModernGalleryZoomableScrollView *_scrollView; + UIView *_scrollContentView; + UIView *_scrollContainerView; + + TGPaintingWrapperView *_paintingWrapperView; + UIView *_drawingView; + + UIPinchGestureRecognizer *_entityPinchGestureRecognizer; + UIRotationGestureRecognizer *_entityRotationGestureRecognizer; + + UIView *_entitiesOutsideContainerView; + UIView *_entitiesWrapperView; + UIView *_entitiesView; + + UIView *_selectionContainerView; + + TGPhotoEditorSparseView *_interfaceWrapperView; + UIViewController *_interfaceController; + + CGSize _previousSize; + CGFloat _keyboardHeight; + TGObserverProxy *_keyboardWillChangeFrameProxy; + + bool _skipEntitiesSetup; + bool _entitiesReady; + + TGPaintingData *_resultData; +} + +@property (nonatomic, weak) PGPhotoEditor *photoEditor; +@property (nonatomic, weak) TGPhotoEditorPreviewView *previewView; + +@end + +@implementation TGPhotoDrawingController + +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView *)entitiesView stickersContext:(id)stickersContext isAvatar:(bool)isAvatar +{ + self = [super initWithContext:context]; + if (self != nil) + { + _context = context; + _stickersContext = stickersContext; + + if (entitiesView != nil) { + _skipEntitiesSetup = true; + entitiesView.userInteractionEnabled = true; + } + + CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]); + _drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize isVideo:photoEditor.forVideo isAvatar:isAvatar entitiesView:entitiesView]; + _interfaceController = (UIViewController *)_drawingAdapter.interfaceController; + + __weak TGPhotoDrawingController *weakSelf = self; + _interfaceController.requestDismiss = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + strongSelf.requestDismiss(); + }; + _interfaceController.requestApply = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + strongSelf.requestApply(); + }; + _interfaceController.getCurrentImage = ^UIImage *{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf.photoEditor currentResultImage]; + }; + _interfaceController.updateVideoPlayback = ^(bool play) { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.controlVideoPlayback(play); + }; + + self.photoEditor = photoEditor; + self.previewView = previewView; + + _keyboardWillChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification]; + } + return self; +} + +- (void)dealloc { + [_context unlockPortrait]; +} + +- (void)loadView +{ + [super loadView]; + self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + _scrollView = [[TGModernGalleryZoomableScrollView alloc] initWithFrame:self.view.bounds hasDoubleTap:false]; + if (@available(iOS 11.0, *)) { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + _scrollView.contentInset = UIEdgeInsetsZero; + _scrollView.delegate = self; + _scrollView.showsHorizontalScrollIndicator = false; + _scrollView.showsVerticalScrollIndicator = false; + [self.view addSubview:_scrollView]; + + _scrollContentView = [[UIView alloc] initWithFrame:self.view.bounds]; + [_scrollView addSubview:_scrollContentView]; + + _scrollContainerView = _drawingAdapter.contentWrapperView; + _scrollContainerView.clipsToBounds = true; +// [_scrollContainerView addTarget:self action:@selector(containerPressed) forControlEvents:UIControlEventTouchUpInside]; + [_scrollContentView addSubview:_scrollContainerView]; + + _entityPinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; + _entityPinchGestureRecognizer.delegate = self; + [_scrollContentView addGestureRecognizer:_entityPinchGestureRecognizer]; + + _entityRotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotate:)]; + _entityRotationGestureRecognizer.delegate = self; + [_scrollContentView addGestureRecognizer:_entityRotationGestureRecognizer]; + + __weak TGPhotoDrawingController *weakSelf = self; + _paintingWrapperView = [[TGPaintingWrapperView alloc] init]; + _paintingWrapperView.clipsToBounds = true; + _paintingWrapperView.shouldReceiveTouch = ^bool + { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return true; + }; + [_scrollContainerView addSubview:_paintingWrapperView]; + + _entitiesOutsideContainerView = [[TGPhotoEditorSparseView alloc] init]; + _entitiesOutsideContainerView.clipsToBounds = true; + [_scrollContainerView addSubview:_entitiesOutsideContainerView]; + + _entitiesWrapperView = [[TGPhotoEditorSparseView alloc] init]; + [_entitiesOutsideContainerView addSubview:_entitiesWrapperView]; + + if (_entitiesView == nil) { + _entitiesView = (UIView *)[_drawingAdapter drawingEntitiesView]; + } + if (!_skipEntitiesSetup) { + [_entitiesWrapperView addSubview:_entitiesView]; + } + + _selectionContainerView = [_drawingAdapter selectionContainerView]; + _selectionContainerView.clipsToBounds = false; + [_scrollContainerView addSubview:_selectionContainerView]; + + _interfaceWrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:_interfaceWrapperView]; + + TGPhotoEditorPreviewView *previewView = _previewView; + previewView.userInteractionEnabled = false; + previewView.hidden = true; + + [_interfaceWrapperView addSubview:_interfaceController.view]; + + if (![self _updateControllerInset:false]) + [self controllerInsetUpdated:UIEdgeInsetsZero]; +} + +- (void)setupCanvas +{ + __weak TGPhotoDrawingController *weakSelf = self; + if (_drawingView == nil) { + _drawingView = (UIView *)_drawingAdapter.drawingView; + _drawingView.zoomOut = ^{ + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_scrollView setZoomScale:strongSelf->_scrollView.normalZoomScale animated:true]; + }; + [_paintingWrapperView addSubview:_drawingView]; + + [_drawingView setupWithDrawingData:_photoEditor.paintingData.drawingData]; + } + + _entitiesView.hasSelectionChanged = ^(bool hasSelection) { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection; + }; + + _entitiesView.getEntityCenterPosition = ^CGPoint { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return CGPointZero; + + return [strongSelf entityCenterPoint]; + }; + + _entitiesView.getEntityInitialRotation = ^CGFloat { + __strong TGPhotoDrawingController *strongSelf = weakSelf; + if (strongSelf == nil) + return 0.0f; + + return [strongSelf entityInitialRotation]; + }; + + [self.view setNeedsLayout]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + PGPhotoEditor *photoEditor = _photoEditor; + if (!_skipEntitiesSetup) { + [_entitiesView setupWithEntitiesData:photoEditor.paintingData.entitiesData]; + } +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [self transitionIn]; +} + +- (void)containerPressed { + [_entitiesView clearSelection]; +} + +- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer +{ + [_entitiesView handlePinch:gestureRecognizer]; +} + +- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer +{ + [_entitiesView handleRotate:gestureRecognizer]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)__unused gestureRecognizer +{ + if (gestureRecognizer == _entityPinchGestureRecognizer && !_entitiesView.hasSelection) { + return false; + } + return !_drawingView.isTracking; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer +{ + return true; +} + +#pragma mark - Tab Bar + +- (TGPhotoEditorTab)availableTabs +{ + return 0; +} + +- (TGPhotoEditorTab)activeTab +{ + return 0; +} + +#pragma mark - Undo & Redo + + +- (TGPaintingData *)_prepareResultData +{ + TGPaintingData *resultData = _resultData; + if (_resultData == nil) { + resultData = [_interfaceController generateResultData]; + _resultData = resultData; + } + return resultData; +} + +- (UIImage *)image +{ + TGPaintingData *paintingData = [self _prepareResultData]; + return paintingData.image; +} + +- (TGPaintingData *)paintingData +{ + return [self _prepareResultData]; +} + +#pragma mark - Scroll View + +- (CGSize)fittedContentSize +{ + return [TGPhotoDrawingController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize]; +} + ++ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize { + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]); + CGFloat scale = fittedOriginalSize.width / originalSize.width; + + CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) + size = CGSizeMake(size.height, size.width); + + return CGSizeMake(floor(size.width), floor(size.height)); +} + +- (CGRect)fittedCropRect:(bool)originalSize +{ + return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize]; +} + ++ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize { + CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]); + CGFloat scale = fittedOriginalSize.width / originalSize.width; + + CGSize size = fittedOriginalSize; + if (!keepOriginalSize) + size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + + return CGRectMake(-cropRect.origin.x * scale, -cropRect.origin.y * scale, size.width, size.height); +} + +- (CGPoint)fittedCropCenterScale:(CGFloat)scale +{ + return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect centerScale:scale]; +} + ++ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale +{ + CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); + CGRect rect = CGRectMake(cropRect.origin.x * scale, cropRect.origin.y * scale, size.width, size.height); + + return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); +} + +- (void)resetScrollView +{ + CGSize fittedContentSize = [self fittedContentSize]; + CGRect fittedCropRect = [self fittedCropRect:false]; + _entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height); + + CGFloat scale = _entitiesOutsideContainerView.bounds.size.width / fittedCropRect.size.width; + _entitiesWrapperView.transform = CGAffineTransformMakeScale(scale, scale); + _entitiesWrapperView.frame = CGRectMake(0.0f, 0.0f, _entitiesOutsideContainerView.bounds.size.width, _entitiesOutsideContainerView.bounds.size.height); + + CGSize contentSize = [self contentSize]; + _scrollView.minimumZoomScale = 1.0f; + _scrollView.maximumZoomScale = 1.0f; + _scrollView.normalZoomScale = 1.0f; + _scrollView.zoomScale = 1.0f; + _scrollView.contentSize = contentSize; + [self contentView].frame = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); + + [self adjustZoom]; + _scrollView.zoomScale = _scrollView.normalZoomScale; +} + +- (void)scrollViewWillBeginZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view +{ +} + +- (void)scrollViewDidZoom:(UIScrollView *)__unused scrollView +{ + [self adjustZoom]; + [_entitiesView onZoom]; +} + +- (void)scrollViewDidEndZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view atScale:(CGFloat)__unused scale +{ + [self adjustZoom]; + + if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON) + { + [TGHacks setAnimationDurationFactor:0.5f]; + [_scrollView setZoomScale:_scrollView.normalZoomScale animated:true]; + [TGHacks setAnimationDurationFactor:1.0f]; + } +} + +- (UIView *)contentView +{ + return _scrollContentView; +} + +- (CGSize)contentSize +{ + return _scrollView.frame.size; +} + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)__unused scrollView +{ + return [self contentView]; +} + +- (void)adjustZoom +{ + CGSize contentSize = [self contentSize]; + CGSize boundsSize = _scrollView.frame.size; + if (contentSize.width < FLT_EPSILON || contentSize.height < FLT_EPSILON || boundsSize.width < FLT_EPSILON || boundsSize.height < FLT_EPSILON) + return; + + CGFloat scaleWidth = boundsSize.width / contentSize.width; + CGFloat scaleHeight = boundsSize.height / contentSize.height; + CGFloat minScale = MIN(scaleWidth, scaleHeight); + CGFloat maxScale = MAX(scaleWidth, scaleHeight); + maxScale = MAX(maxScale, minScale * 3.0f); + + if (ABS(maxScale - minScale) < 0.01f) + maxScale = minScale; + + _scrollView.contentInset = UIEdgeInsetsZero; + + if (_scrollView.minimumZoomScale != 0.05f) + _scrollView.minimumZoomScale = 0.05f; + if (_scrollView.normalZoomScale != minScale) + _scrollView.normalZoomScale = minScale; + if (_scrollView.maximumZoomScale != maxScale) + _scrollView.maximumZoomScale = maxScale; + + CGRect contentFrame = [self contentView].frame; + + if (boundsSize.width > contentFrame.size.width) + contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0f; + else + contentFrame.origin.x = 0; + + if (boundsSize.height > contentFrame.size.height) + contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0f; + else + contentFrame.origin.y = 0; + + [self contentView].frame = contentFrame; + + _scrollView.scrollEnabled = ABS(_scrollView.zoomScale - _scrollView.normalZoomScale) > FLT_EPSILON; + + [_drawingView updateZoomScale:_scrollView.zoomScale]; +} + +#pragma mark - Transitions + +- (void)transitionIn { + [_context lockPortrait]; + [_context disableInteractiveKeyboardGesture]; +// if (self.presentedForAvatarCreation) { +// _drawingView.hidden = true; +// } +} + ++ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation +{ + CGRect frame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:parentViewFrame toolbarLandscapeSize:toolbarLandscapeSize orientation:orientation panelSize:panelSize hasOnScreenNavigation:hasOnScreenNavigation]; + + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + frame.origin.x -= TGPhotoPaintTopPanelSize; + break; + + case UIInterfaceOrientationLandscapeRight: + frame.origin.x += TGPhotoPaintTopPanelSize; + break; + + default: + frame.origin.y += TGPhotoPaintTopPanelSize; + break; + } + + return frame; +} + +- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame +{ + CGSize referenceSize = [self referenceViewSize]; + CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size); + CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + + return toFrame; +} + +- (void)_finishedTransitionInWithView:(UIView *)transitionView +{ + if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) { + + } else { + [transitionView removeFromSuperview]; + } + + [self setupCanvas]; + _entitiesView.hidden = false; + + TGPhotoEditorPreviewView *previewView = _previewView; + [previewView setPaintingHidden:true]; + previewView.hidden = false; + [_scrollContainerView insertSubview:previewView belowSubview:_paintingWrapperView]; + [self updateContentViewLayout]; + [previewView performTransitionInIfNeeded]; + + CGRect rect = [self fittedCropRect:true]; + _entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); + _entitiesView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation); + + CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]); + CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation); + CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); + + CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; + CGPoint offset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); + + CGPoint boundsCenter = TGPaintCenterOfRect(_entitiesWrapperView.bounds); + _entitiesView.center = TGPaintAddPoints(boundsCenter, offset); + + if (!_skipEntitiesSetup || _entitiesReady) { + [_entitiesWrapperView addSubview:_entitiesView]; + } + _entitiesReady = true; + [self resetScrollView]; +} + +- (void)prepareForCustomTransitionOut +{ + _previewView.hidden = true; + _drawingView.hidden = true; + _entitiesOutsideContainerView.hidden = true; +} + +- (void)transitionOutSwitching:(bool)__unused switching completion:(void (^)(void))completion +{ + TGPhotoEditorPreviewView *previewView = self.previewView; + previewView.interactionEnded = nil; + + [_interfaceController animateOut:^{ + if (completion != nil) + completion(); + }]; +} + +- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation +{ + CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size); + return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); +} + +- (void)_animatePreviewViewTransitionOutToFrame:(CGRect)targetFrame saving:(bool)saving parentView:(UIView *)parentView completion:(void (^)(void))completion +{ + _dismissing = true; + +// [_entitySelectionView removeFromSuperview]; +// _entitySelectionView = nil; + + TGPhotoEditorPreviewView *previewView = self.previewView; + [previewView prepareForTransitionOut]; + + UIInterfaceOrientation orientation = self.effectiveOrientation; + CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height); + CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y); + previewView.frame = rect; + + UIView *snapshotView = nil; + POPSpringAnimation *snapshotAnimation = nil; + NSMutableArray *animations = [[NSMutableArray alloc] init]; + + if (saving && CGRectIsNull(targetFrame) && parentView != nil) + { + snapshotView = [previewView snapshotViewAfterScreenUpdates:false]; + snapshotView.frame = [_scrollContainerView convertRect:previewView.frame toView:parentView]; + + UIView *canvasSnapshotView = [_paintingWrapperView resizableSnapshotViewFromRect:[_paintingWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; + canvasSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + canvasSnapshotView.transform = _entitiesOutsideContainerView.transform; + canvasSnapshotView.frame = snapshotView.bounds; + [snapshotView addSubview:canvasSnapshotView]; + + UIView *entitiesSnapshotView = [_entitiesWrapperView resizableSnapshotViewFromRect:[_entitiesWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; + entitiesSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + entitiesSnapshotView.transform = _entitiesOutsideContainerView.transform; + entitiesSnapshotView.frame = snapshotView.bounds; + [snapshotView addSubview:entitiesSnapshotView]; + + CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size); + targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + + [parentView addSubview:snapshotView]; + + snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame]; + snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; + [animations addObject:snapshotAnimation]; + } + + targetFrame = CGRectOffset(targetFrame, -containerFrame.origin.x, -containerFrame.origin.y); + CGPoint targetCenter = TGPaintCenterOfRect(targetFrame); + + POPSpringAnimation *previewAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; + previewAnimation.fromValue = [NSValue valueWithCGRect:previewView.frame]; + previewAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; + [animations addObject:previewAnimation]; + + POPSpringAnimation *previewAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + previewAlphaAnimation.fromValue = @(previewView.alpha); + previewAlphaAnimation.toValue = @(0.0f); + [animations addObject:previewAnimation]; + + POPSpringAnimation *entitiesAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; + entitiesAnimation.fromValue = [NSValue valueWithCGPoint:_entitiesOutsideContainerView.center]; + entitiesAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; + [animations addObject:entitiesAnimation]; + + CGFloat targetEntitiesScale = targetFrame.size.width / _entitiesOutsideContainerView.frame.size.width; + POPSpringAnimation *entitiesScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; + entitiesScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; + entitiesScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetEntitiesScale, targetEntitiesScale)]; + [animations addObject:entitiesScaleAnimation]; + + POPSpringAnimation *entitiesAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + entitiesAlphaAnimation.fromValue = @(_drawingView.alpha); + entitiesAlphaAnimation.toValue = @(0.0f); + [animations addObject:entitiesAlphaAnimation]; + + POPSpringAnimation *paintingAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; + paintingAnimation.fromValue = [NSValue valueWithCGPoint:_paintingWrapperView.center]; + paintingAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; + [animations addObject:paintingAnimation]; + + CGFloat targetPaintingScale = targetFrame.size.width / _paintingWrapperView.frame.size.width; + POPSpringAnimation *paintingScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; + paintingScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; + paintingScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetPaintingScale, targetPaintingScale)]; + [animations addObject:paintingScaleAnimation]; + + POPSpringAnimation *paintingAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; + paintingAlphaAnimation.fromValue = @(_paintingWrapperView.alpha); + paintingAlphaAnimation.toValue = @(0.0f); + [animations addObject:paintingAlphaAnimation]; + + [TGPhotoEditorAnimation performBlock:^(__unused bool allFinished) + { + [snapshotView removeFromSuperview]; + + if (completion != nil) + completion(); + } whenCompletedAllAnimations:animations]; + + if (snapshotAnimation != nil) + [snapshotView pop_addAnimation:snapshotAnimation forKey:@"frame"]; + [previewView pop_addAnimation:previewAnimation forKey:@"frame"]; + [previewView pop_addAnimation:previewAlphaAnimation forKey:@"alpha"]; + + [_entitiesOutsideContainerView pop_addAnimation:entitiesAnimation forKey:@"frame"]; + [_entitiesOutsideContainerView pop_addAnimation:entitiesScaleAnimation forKey:@"scale"]; + [_entitiesOutsideContainerView pop_addAnimation:entitiesAlphaAnimation forKey:@"alpha"]; + + [_paintingWrapperView pop_addAnimation:paintingAnimation forKey:@"frame"]; + [_paintingWrapperView pop_addAnimation:paintingScaleAnimation forKey:@"scale"]; + [_paintingWrapperView pop_addAnimation:paintingAlphaAnimation forKey:@"alpha"]; + + if (saving) + { + _entitiesOutsideContainerView.hidden = true; + _paintingWrapperView.hidden = true; + previewView.hidden = true; + } +} + +- (CGRect)transitionOutReferenceFrame +{ + TGPhotoEditorPreviewView *previewView = _previewView; + return [previewView convertRect:previewView.bounds toView:self.view]; +} + +- (UIView *)transitionOutReferenceView +{ + return _previewView; +} + +- (UIView *)snapshotView +{ + TGPhotoEditorPreviewView *previewView = self.previewView; + return [previewView originalSnapshotView]; +} + +- (id)currentResultRepresentation +{ + return TGPaintCombineCroppedImages(self.photoEditor.currentResultImage, [self image], true, _photoEditor.originalSize, _photoEditor.cropRect, _photoEditor.cropOrientation, _photoEditor.cropRotation, false); +} + +#pragma mark - Layout + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + [self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + [self updateLayout:toInterfaceOrientation]; +} + +- (void)updateContentViewLayout +{ + CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(_photoEditor.cropOrientation)); + _entitiesOutsideContainerView.transform = rotationTransform; + _entitiesOutsideContainerView.frame = self.previewView.frame; + [self resetScrollView]; +} + +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + UIView *parentView = self.view; + + NSTimeInterval duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil ? 0.3 : [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]; + CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGRect keyboardFrame = [parentView convertRect:screenKeyboardFrame fromView:nil]; + + CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (parentView.frame.size.height - keyboardFrame.origin.y); + keyboardHeight = MAX(keyboardHeight, 0.0f); + + _keyboardHeight = keyboardHeight; + + CGSize referenceSize = [self referenceViewSize]; + CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; + + CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + CGFloat topInset = [self controllerStatusBarHeight] + 31.0; + CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset; + CGFloat yCenter = visibleArea / 2.0f; + CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset; + CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f; + + [UIView animateWithDuration:duration delay:0.0 options:curve animations:^ + { + _interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, _interfaceWrapperView.frame.size.width, _interfaceWrapperView.frame.size.height); + _scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); + } completion:nil]; + + [self updateInterfaceLayoutAnimated:true]; +} + +- (void)updateInterfaceLayoutAnimated:(BOOL)animated { + if (_interfaceController == nil) + return; + + CGSize size = [self referenceViewSize]; + _interfaceController.view.frame = CGRectMake((_interfaceWrapperView.frame.size.width - size.width) / 2.0, (_interfaceWrapperView.frame.size.height - size.height) / 2.0, size.width, size.height); + [_interfaceController adapterContainerLayoutUpdatedSize:[self referenceViewSize] + intrinsicInsets:_context.safeAreaInset + safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right) + statusBarHeight:[_context statusBarFrame].size.height + inputHeight:_keyboardHeight + orientation:self.effectiveOrientation + isRegular:[UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad + animated:animated]; + +} + +- (void)updateLayout:(UIInterfaceOrientation)orientation +{ + if ([self inFormSheet] || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + { + orientation = UIInterfaceOrientationPortrait; + } + + CGSize referenceSize = [self referenceViewSize]; + CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; + + bool sizeUpdated = false; + if (!CGSizeEqualToSize(referenceSize, _previousSize)) { + sizeUpdated = true; + _previousSize = referenceSize; + } + + UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:self.hasOnScreenNavigation]; + UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - referenceSize.height) / 2, (screenSide - referenceSize.width) / 2, (screenSide + referenceSize.height) / 2, (screenSide + referenceSize.width) / 2); + screenEdges.top += safeAreaInset.top; + screenEdges.left += safeAreaInset.left; + screenEdges.bottom -= safeAreaInset.bottom; + screenEdges.right -= safeAreaInset.right; + + CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; + + PGPhotoEditor *photoEditor = self.photoEditor; + TGPhotoEditorPreviewView *previewView = self.previewView; + + CGSize fittedSize = TGScaleToSize(photoEditor.rotatedCropSize, containerFrame.size); + CGRect previewFrame = CGRectMake((containerFrame.size.width - fittedSize.width) / 2, (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + _drawingView.screenSize = fittedSize; + + CGFloat topInset = [self controllerStatusBarHeight] + 31.0; + CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset; + CGFloat yCenter = visibleArea / 2.0f; + CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y + topInset; + CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f; + + _interfaceWrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2, screenSide, screenSide); + [self updateInterfaceLayoutAnimated:false]; + + if (_dismissing || (previewView.superview != _scrollContainerView && previewView.superview != self.view)) + return; + + if (previewView.superview == self.view) + { + previewFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); + } + + UIImageOrientation cropOrientation = _photoEditor.cropOrientation; + CGRect cropRect = _photoEditor.cropRect; + CGSize originalSize = _photoEditor.originalSize; + CGFloat rotation = _photoEditor.cropRotation; + + CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); + _entitiesOutsideContainerView.transform = rotationTransform; + _entitiesOutsideContainerView.frame = previewFrame; + + _scrollView.frame = self.view.bounds; + + if (sizeUpdated) { + [self resetScrollView]; + } + [self adjustZoom]; + + _paintingWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); + _paintingWrapperView.frame = previewFrame; + + CGFloat originalWidth = TGOrientationIsSideward(cropOrientation, NULL) ? previewFrame.size.height : previewFrame.size.width; + CGFloat ratio = originalWidth / cropRect.size.width; + CGRect originalFrame = CGRectMake(-cropRect.origin.x * ratio, -cropRect.origin.y * ratio, originalSize.width * ratio, originalSize.height * ratio); + + previewView.frame = previewFrame; + + if ([self presentedForAvatarCreation]) { + CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForOrientation(photoEditor.cropOrientation)); + if (photoEditor.cropMirrored) + transform = CGAffineTransformScale(transform, -1.0f, 1.0f); + previewView.transform = transform; + } + + CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio); + CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation); + CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); + + CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; + CGPoint centerOffset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); + + _drawingView.transform = CGAffineTransformIdentity; + _drawingView.frame = originalFrame; + _drawingView.transform = CGAffineTransformMakeRotation(rotation); + _drawingView.center = TGPaintAddPoints(TGPaintCenterOfRect(_paintingWrapperView.bounds), centerOffset); + + _selectionContainerView.transform = CGAffineTransformRotate(rotationTransform, rotation); + _selectionContainerView.frame = previewFrame; + + _scrollContainerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + return UIRectEdgeTop | UIRectEdgeBottom; +} + +- (CGPoint)entityCenterPoint +{ + //return [_scrollView convertPoint:TGPaintCenterOfRect(_scrollView.bounds) toView:_entitiesView]; + return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView]; +} + +- (CGFloat)entityInitialRotation +{ + return TGCounterRotationForOrientation(_photoEditor.cropOrientation) - _photoEditor.cropRotation; +} + ++ (CGSize)maximumPaintingSize +{ + static dispatch_once_t onceToken; + static CGSize size; + dispatch_once(&onceToken, ^ + { + CGSize screenSize = TGScreenSize(); + if ((NSInteger)screenSize.height == 480) + size = TGPhotoPaintingLightMaxSize; + else + size = TGPhotoPaintingMaxSize; + }); + return size; +} + +@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index 2293be6c683..0ca5fa708b7 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -4,8 +4,6 @@ #import -#import - #import #import @@ -29,7 +27,6 @@ #import "TGPhotoToolbarView.h" #import "TGPhotoEditorPreviewView.h" -#import "TGPhotoEntitiesContainerView.h" #import @@ -38,7 +35,7 @@ #import "TGPhotoCropController.h" #import "TGPhotoToolsController.h" -#import "TGPhotoPaintController.h" +#import "TGPhotoDrawingController.h" #import "TGPhotoQualityController.h" #import "TGPhotoAvatarPreviewController.h" @@ -53,7 +50,7 @@ #import #import "TGCameraCapturedVideo.h" -@interface TGPhotoEditorController () +@interface TGPhotoEditorController () { bool _switchingTab; TGPhotoEditorTab _availableTabs; @@ -70,7 +67,7 @@ @interface TGPhotoEditorController () *_fullEntitiesView; UIImageView *_fullPaintingView; PGPhotoEditor *_photoEditor; @@ -103,7 +100,6 @@ @interface TGPhotoEditorController () )context item:(id)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab { self = [super initWithContext:context]; if (self != nil) { _context = context; - _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; - + self.automaticallyManageScrollViewInsets = false; self.autoManageStatusBarBackground = false; self.isImportant = true; @@ -196,7 +189,6 @@ - (instancetype)initWithContext:(id)context item:(id_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - [strongSelf->_currentTabController handleTabAction:tab]; - else - [strongSelf presentTab:TGPhotoEditorPaintTab]; + [strongSelf presentTab:TGPhotoEditorPaintTab]; break; case TGPhotoEditorStickerTab: @@ -352,9 +336,10 @@ - (void)loadView _fullPaintingView = [[UIImageView alloc] init]; _fullPaintingView.frame = _fullPreviewView.frame; - _fullEntitiesView = [[TGPhotoEntitiesContainerView alloc] init]; + CGSize size = TGScaleToSize(_photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]); + _fullEntitiesView = [_stickersContext drawingEntitiesViewWithSize:size]; _fullEntitiesView.userInteractionEnabled = false; - CGRect rect = [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true]; + CGRect rect = [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true]; _fullEntitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); } @@ -710,7 +695,7 @@ - (void)startVideoPlayback:(bool)reset { startPosition = self.trimStartValue; CMTime targetTime = CMTimeMakeWithSeconds(startPosition, NSEC_PER_SEC); - [_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; + [_player.currentItem seekToTime:targetTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:nil]; [self _setupPlaybackStartedObserver]; @@ -951,7 +936,7 @@ - (void)createEditedImageWithEditorValues:(id)editorValu self.willFinishEditing(nil, [_currentTabController currentResultRepresentation], true); if (self.didFinishEditing != nil) - self.didFinishEditing(nil, nil, nil, true); + self.didFinishEditing(nil, nil, nil, true, ^{}); if (completion != nil) completion(nil); @@ -1058,7 +1043,7 @@ - (void)createEditedImageWithEditorValues:(id)editorValu else { void (^didFinishRenderingFullSizeImage)(UIImage *) = self.didFinishRenderingFullSizeImage; - void (^didFinishEditing)(id, UIImage *, UIImage *, bool ) = self.didFinishEditing; + void (^didFinishEditing)(id, UIImage *, UIImage *, bool , void(^)(void)) = self.didFinishEditing; [[[[renderedImageSignal map:^id(UIImage *image) { @@ -1113,12 +1098,13 @@ - (void)createEditedImageWithEditorValues:(id)editorValu image = TGScaleImageToPixelSize(image, CGSizeMake(150.0, 150.0)); } - if (avatar && completion != nil) { - completion(image); + if (!saveOnly && didFinishEditing != nil) { + didFinishEditing(editorValues, image, thumbnailImage, true, ^{ + if (avatar && completion != nil) { + completion(image); + } + }); } - - if (!saveOnly && didFinishEditing != nil) - didFinishEditing(editorValues, image, thumbnailImage, true); } error:^(__unused id error) { TGLegacyLog(@"renderedImageSignal error"); @@ -1143,6 +1129,16 @@ - (bool)presentedForForumAvatarCreation return _intent & (TGPhotoEditorControllerForumAvatarIntent); } +- (bool)presentedForSuggestedAvatar +{ + return _intent & (TGPhotoEditorControllerSuggestedAvatarIntent); +} + +- (bool)presentedForSuggestingAvatar +{ + return _intent & (TGPhotoEditorControllerSuggestingAvatarIntent); +} + #pragma mark - Transition - (void)transitionIn @@ -1232,7 +1228,7 @@ - (void)presentTab:(TGPhotoEditorTab)tab [self savePaintingData]; bool resetTransform = false; - if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoPaintController class]]) { + if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoDrawingController class]]) { resetTransform = true; } @@ -1315,6 +1311,8 @@ - (void)presentTab:(TGPhotoEditorTab)tab TGPhotoEditorBackButton backButtonType = TGPhotoEditorBackButtonCancel; TGPhotoEditorDoneButton doneButtonType = TGPhotoEditorDoneButtonCheck; + bool sideButtonsHiddenInCrop = [self presentedForSuggestedAvatar] && !_item.isVideo; + __weak TGPhotoEditorController *weakSelf = self; TGPhotoEditorTabController *controller = nil; switch (tab) @@ -1328,9 +1326,12 @@ - (void)presentTab:(TGPhotoEditorTab)tab if ([self presentedForAvatarCreation]) { + [_containerView.superview insertSubview:_containerView belowSubview:_portraitToolbarView]; + bool skipInitialTransition = (![self presentedFromCamera] && self.navigationController != nil) || self.skipInitialTransition; - TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation]]; + TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar] isSuggesting:[self presentedForSuggestingAvatar] senderName:self.senderName]; + cropController.stickersContext = _stickersContext; cropController.scrubberView = _scrubberView; cropController.dotImageView = _dotImageView; cropController.dotMarkerView = _dotMarkerView; @@ -1458,9 +1459,32 @@ - (void)presentTab:(TGPhotoEditorTab)tab [strongSelf returnFullPreviewView]; }; + cropController.cancelPressed = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_portraitToolbarView.cancelPressed(); + }; + cropController.donePressed = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_portraitToolbarView.donePressed(); + }; controller = cropController; doneButtonType = TGPhotoEditorDoneButtonDone; + + if (sideButtonsHiddenInCrop) { + [_portraitToolbarView setCancelDoneButtonsHidden:true animated:true]; + [_portraitToolbarView setCenterButtonsHidden:false animated:true]; + [_landscapeToolbarView setAllButtonsHidden:false animated:true]; + } else { + [_portraitToolbarView setAllButtonsHidden:false animated:false]; + [_landscapeToolbarView setAllButtonsHidden:false animated:false]; + } } else { @@ -1560,10 +1584,30 @@ - (void)presentTab:(TGPhotoEditorTab)tab case TGPhotoEditorPaintTab: { - TGPhotoPaintController *paintController = [[TGPhotoPaintController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView]; - paintController.stickersContext = _stickersContext; - paintController.toolbarLandscapeSize = TGPhotoEditorToolbarSize; - paintController.controlVideoPlayback = ^(bool play) { + [_portraitToolbarView setAllButtonsHidden:true animated:[self presentedForAvatarCreation]]; + [_landscapeToolbarView setAllButtonsHidden:true animated:[self presentedForAvatarCreation]]; + + [_containerView.superview bringSubviewToFront:_containerView]; + + TGPhotoDrawingController *drawingController = [[TGPhotoDrawingController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView stickersContext:_stickersContext isAvatar:[self presentedForAvatarCreation]]; + drawingController.requestDismiss = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + [strongSelf dismissEditor]; + }; + drawingController.requestApply = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + if ([strongSelf presentedForAvatarCreation]) { + [strongSelf presentTab:TGPhotoEditorCropTab]; + } else { + [strongSelf applyEditor]; + } + }; + drawingController.toolbarLandscapeSize = TGPhotoEditorToolbarSize; + drawingController.controlVideoPlayback = ^(bool play) { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -1573,7 +1617,7 @@ - (void)presentTab:(TGPhotoEditorTab)tab [strongSelf stopVideoPlayback:false]; } }; - paintController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView) + drawingController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView) { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -1585,7 +1629,7 @@ - (void)presentTab:(TGPhotoEditorTab)tab return transitionReferenceView; }; - paintController.finishedTransitionIn = ^ + drawingController.finishedTransitionIn = ^ { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -1599,13 +1643,26 @@ - (void)presentTab:(TGPhotoEditorTab)tab if (isInitialAppearance) [strongSelf startVideoPlayback:true]; }; + drawingController.finishedTransitionOut = ^{ + __strong TGPhotoEditorController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_containerView.superview insertSubview:strongSelf->_containerView atIndex:2]; + [strongSelf->_portraitToolbarView setAllButtonsHidden:false animated:true]; + [strongSelf->_landscapeToolbarView setAllButtonsHidden:false animated:true]; + }; - controller = paintController; + controller = drawingController; } break; case TGPhotoEditorToolsTab: { + if ([self presentedForSuggestedAvatar]) { + [_portraitToolbarView setCancelDoneButtonsHidden:false animated:true]; + } + TGPhotoToolsController *toolsController = [[TGPhotoToolsController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView entitiesView:_fullEntitiesView]; toolsController.toolbarLandscapeSize = TGPhotoEditorToolbarSize; toolsController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView, bool *noTransitionView) @@ -1680,7 +1737,7 @@ - (void)presentTab:(TGPhotoEditorTab)tab _currentTabController.switchingFromTab = switchingFromTab; _currentTabController.initialAppearance = isInitialAppearance; - if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) _currentTabController.availableTabs = _availableTabs; if ([self presentedForAvatarCreation] && self.navigationController == nil) @@ -1873,12 +1930,17 @@ - (void)dismissEditor strongSelf.willFinishEditing(nil, nil, false); if (strongSelf.didFinishEditing != nil) - strongSelf.didFinishEditing(nil, nil, nil, false); + strongSelf.didFinishEditing(nil, nil, nil, false, ^{}); }; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) { + dismiss(); + return; + } + TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; @@ -1888,7 +1950,7 @@ - (void)dismissEditor controller.dismissesByOutsideTap = true; controller.narrowInLandscape = true; __weak TGMenuSheetController *weakController = controller; - + NSArray *items = @ [ [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ @@ -1896,7 +1958,7 @@ - (void)dismissEditor __strong TGMenuSheetController *strongController = weakController; if (strongController == nil) return; - + [strongController dismissAnimated:true manual:false completion:^ { dismiss(); @@ -1909,14 +1971,14 @@ - (void)dismissEditor [strongController dismissAnimated:true]; }] ]; - + [controller setItemViews:items]; controller.sourceRect = ^ { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) return CGRectZero; - + if (UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation)) return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView]; else @@ -1940,10 +2002,10 @@ - (void)doneButtonPressed } - (void)savePaintingData { - if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if (![_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) return; - TGPhotoPaintController *paintController = (TGPhotoPaintController *)_currentTabController; + TGPhotoDrawingController *paintController = (TGPhotoDrawingController *)_currentTabController; TGPaintingData *paintingData = [paintController paintingData]; _photoEditor.paintingData = paintingData; @@ -1959,7 +2021,11 @@ - (void)applyEditor if (![_currentTabController isDismissAllowed]) return; - self.view.userInteractionEnabled = false; + bool forAvatar = [self presentedForAvatarCreation]; + + if (!forAvatar) { + self.view.userInteractionEnabled = false; + } [_currentTabController prepareTransitionOutSaving:true]; bool saving = true; @@ -1967,7 +2033,7 @@ - (void)applyEditor NSTimeInterval trimStartValue = 0.0; NSTimeInterval trimEndValue = 0.0; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) { [self savePaintingData]; } @@ -2062,9 +2128,9 @@ - (void)applyEditor TGDispatchOnMainThread(^{ if (self.didFinishEditingVideo != nil) - self.didFinishEditingVideo(asset, [adjustments editAdjustmentsWithPreset:preset videoStartValue:videoStartValue trimStartValue:trimStartValue trimEndValue:trimEndValue], fullImage, nil, true); - - [self dismissAnimated:true]; + self.didFinishEditingVideo(asset, [adjustments editAdjustmentsWithPreset:preset videoStartValue:videoStartValue trimStartValue:trimStartValue trimEndValue:trimEndValue], fullImage, nil, true, ^{ + [self dismissAnimated:true]; + }); }); }]; }]; @@ -2072,11 +2138,13 @@ - (void)applyEditor } else if (_intent != TGPhotoEditorControllerVideoIntent) { - TGProgressWindow *progressWindow = [[TGProgressWindow alloc] init]; - progressWindow.windowLevel = self.view.window.windowLevel + 0.001f; - [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; + TGProgressWindow *progressWindow; + if (!forAvatar) { + progressWindow = [[TGProgressWindow alloc] init]; + progressWindow.windowLevel = self.view.window.windowLevel + 0.001f; + [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; + } - bool forAvatar = [self presentedForAvatarCreation]; [self createEditedImageWithEditorValues:adjustments createThumbnail:!forAvatar saveOnly:false completion:^(__unused UIImage *image) { [NSObject cancelPreviousPerformRequestsWithTarget:progressWindow selector:@selector(showAnimated) object:nil]; @@ -2170,16 +2238,16 @@ - (void)applyEditor if (self.willFinishEditing != nil) self.willFinishEditing(hasChanges ? adjustments : nil, nil, hasChanges); - if (self.didFinishEditing != nil) - self.didFinishEditing(hasChanges ? adjustments : nil, nil, nil, hasChanges); - - if ([self presentedForAvatarCreation]) { - [self dismissAnimated:true]; - } else { - [self transitionOutSaving:saving completion:^ - { - [self dismiss]; - }]; + if (self.didFinishEditing != nil) { + self.didFinishEditing(hasChanges ? adjustments : nil, nil, nil, hasChanges, ^{ + if ([self presentedForAvatarCreation]) { + [self dismissAnimated:true]; + } else { + [self transitionOutSaving:saving completion:^{ + [self dismiss]; + }]; + } + }); } } } @@ -2196,45 +2264,6 @@ - (TGMediaEditingContext *)editingContext } } -- (void)doneButtonLongPressed:(UIButton *)sender -{ - if (_intent == TGPhotoEditorControllerVideoIntent) - return; - - if (_menuContainerView != nil) - { - [_menuContainerView removeFromSuperview]; - _menuContainerView = nil; - } - - _menuContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.view.frame.size.width, self.view.frame.size.height)]; - [self.view addSubview:_menuContainerView]; - - NSMutableArray *actions = [[NSMutableArray alloc] init]; - [actions addObject:@{ @"title": @"Save to Camera Roll", @"action": @"save" }]; - if ([_context canOpenURL:[NSURL URLWithString:@"instagram://"]]) - [actions addObject:@{ @"title": @"Share on Instagram", @"action": @"instagram" }]; - - [_menuContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle]; - [_menuContainerView.menuView sizeToFit]; - - CGRect titleLockIconViewFrame = [sender.superview convertRect:sender.frame toView:_menuContainerView]; - titleLockIconViewFrame.origin.y += 16.0f; - [_menuContainerView showMenuFromRect:titleLockIconViewFrame animated:false]; -} - -- (void)actionStageActionRequested:(NSString *)action options:(id)options -{ - if ([action isEqualToString:@"menuAction"]) - { - NSString *menuAction = options[@"action"]; - if ([menuAction isEqualToString:@"save"]) - [self _saveToCameraRoll]; - else if ([menuAction isEqualToString:@"instagram"]) - [self _openInInstagram]; - } -} - #pragma mark - External Export - (void)_saveToCameraRoll @@ -2244,8 +2273,8 @@ - (void)_saveToCameraRoll [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; @@ -2266,8 +2295,8 @@ - (void)_openInInstagram [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; TGPaintingData *paintingData = nil; - if ([_currentTabController isKindOfClass:[TGPhotoPaintController class]]) - paintingData = [(TGPhotoPaintController *)_currentTabController paintingData]; + if ([_currentTabController isKindOfClass:[TGPhotoDrawingController class]]) + paintingData = [(TGPhotoDrawingController *)_currentTabController paintingData]; PGPhotoEditorValues *editorValues = paintingData == nil ? [_photoEditor exportAdjustments] : [_photoEditor exportAdjustmentsWithPaintingData:paintingData]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h deleted file mode 100644 index 00e2f53e883..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.h +++ /dev/null @@ -1,38 +0,0 @@ -#import "TGPhotoEditorSparseView.h" -#import "TGPhotoPaintStickersContext.h" - -@class TGPaintingData; -@class TGPhotoPaintEntity; -@class TGPhotoPaintEntityView; - -@interface TGPhotoEntitiesContainerView : TGPhotoEditorSparseView - -@property (nonatomic, strong) id stickersContext; - -@property (nonatomic, readonly) NSUInteger entitiesCount; -@property (nonatomic, copy) void (^entitySelected)(TGPhotoPaintEntityView *); -@property (nonatomic, copy) void (^entityRemoved)(TGPhotoPaintEntityView *); - -- (void)updateVisibility:(bool)visible; -- (void)seekTo:(double)timestamp; -- (void)play; -- (void)pause; -- (void)resetToStart; - -- (UIColor *)colorAtPoint:(CGPoint)point; - -- (void)setupWithPaintingData:(TGPaintingData *)paintingData; -- (TGPhotoPaintEntityView *)createEntityViewWithEntity:(TGPhotoPaintEntity *)entity; - -- (TGPhotoPaintEntityView *)viewForUUID:(NSInteger)uuid; -- (void)removeViewWithUUID:(NSInteger)uuid; -- (void)removeAll; - -- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer; -- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer; - -- (UIImage *)imageInRect:(CGRect)rect background:(UIImage *)background still:(bool)still; - -- (bool)isTrackingAnyEntityView; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m b/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m deleted file mode 100644 index b0c3d3d654f..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoEntitiesContainerView.m +++ /dev/null @@ -1,453 +0,0 @@ -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoPaintEntityView.h" -#import "TGPhotoStickerEntityView.h" -#import "TGPhotoTextEntityView.h" -#import "TGPaintingData.h" - -#import - -@interface TGPhotoEntitiesContainerView () -{ - TGPhotoPaintEntityView *_currentView; - UITapGestureRecognizer *_tapGestureRecognizer; -} -@end - -@implementation TGPhotoEntitiesContainerView - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; - _tapGestureRecognizer.delegate = self; - [self addGestureRecognizer:_tapGestureRecognizer]; - } - return self; -} - -- (void)updateVisibility:(bool)visible -{ - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - [(TGPhotoStickerEntityView *)view updateVisibility:visible]; - } - } -} - -- (void)seekTo:(double)timestamp { - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - [(TGPhotoStickerEntityView *)view seekTo:timestamp]; - } - } -} - -- (void)play { - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - [(TGPhotoStickerEntityView *)view play]; - } - } -} - -- (void)pause { - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - [(TGPhotoStickerEntityView *)view pause]; - } - } -} - - -- (void)resetToStart { - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - [(TGPhotoStickerEntityView *)view resetToStart]; - } - } -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer -{ - return false; -} - -- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer -{ - CGPoint point = [gestureRecognizer locationInView:self]; - - NSMutableArray *intersectedViews = [[NSMutableArray alloc] init]; - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view pointInside:[view convertPoint:point fromView:self] withEvent:nil]) - [intersectedViews addObject:view]; - } - - TGPhotoPaintEntityView *result = nil; - if (intersectedViews.count > 1) - { - __block TGPhotoPaintEntityView *subresult = nil; - [intersectedViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(TGPhotoPaintEntityView *view, __unused NSUInteger index, BOOL *stop) - { - if ([view precisePointInside:[view convertPoint:point fromView:self]]) - { - subresult = view; - *stop = true; - } - }]; - - result = subresult ?: intersectedViews.lastObject; - } - else if (intersectedViews.count == 1) - { - result = intersectedViews.firstObject; - } - - if (self.entitySelected != nil) - self.entitySelected(result); -} - -- (UIColor *)colorAtPoint:(CGPoint)point { - NSMutableArray *intersectedViews = [[NSMutableArray alloc] init]; - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view pointInside:[view convertPoint:point fromView:self] withEvent:nil]) - [intersectedViews addObject:view]; - } - - TGPhotoPaintEntityView *result = nil; - if (intersectedViews.count > 1) - { - __block TGPhotoPaintEntityView *subresult = nil; - [intersectedViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(TGPhotoPaintEntityView *view, __unused NSUInteger index, BOOL *stop) - { - if ([view precisePointInside:[view convertPoint:point fromView:self]]) - { - subresult = view; - *stop = true; - } - }]; - - result = subresult ?: intersectedViews.lastObject; - } - else if (intersectedViews.count == 1) - { - result = intersectedViews.firstObject; - } - - return [result colorAtPoint:[result convertPoint:point fromView:self]]; -} - -- (NSUInteger)entitiesCount -{ - return MAX(0, (NSInteger)self.subviews.count - 1); -} - -- (void)setupWithPaintingData:(TGPaintingData *)paintingData { - [self removeAll]; - for (TGPhotoPaintEntity *entity in paintingData.entities) { - UIView * entityView = [self createEntityViewWithEntity:entity]; - [self addSubview:entityView]; - } -} - -- (TGPhotoPaintEntityView *)createEntityViewWithEntity:(TGPhotoPaintEntity *)entity { - if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) - return [self _createStickerViewWithEntity:(TGPhotoPaintStickerEntity *)entity]; - else if ([entity isKindOfClass:[TGPhotoPaintTextEntity class]]) - return [self _createTextViewWithEntity:(TGPhotoPaintTextEntity *)entity]; - - return nil; -} - -- (TGPhotoStickerEntityView *)_createStickerViewWithEntity:(TGPhotoPaintStickerEntity *)entity -{ - TGPhotoStickerEntityView *stickerView = [[TGPhotoStickerEntityView alloc] initWithEntity:entity context:self.stickersContext]; - [self _commonEntityViewSetup:stickerView entity:entity]; - - return stickerView; -} - -- (TGPhotoTextEntityView *)_createTextViewWithEntity:(TGPhotoPaintTextEntity *)entity -{ - TGPhotoTextEntityView *textView = [[TGPhotoTextEntityView alloc] initWithEntity:entity]; - [textView sizeToFit]; - - [self _commonEntityViewSetup:textView entity:entity]; - - return textView; -} - -- (void)_commonEntityViewSetup:(TGPhotoPaintEntityView *)entityView entity:(TGPhotoPaintEntity *)entity -{ - entityView.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(entity.scale, entity.scale), entity.angle); - entityView.center = entity.position; -} - -- (TGPhotoPaintEntityView *)viewForUUID:(NSInteger)uuid -{ - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if (view.entityUUID == uuid) - return view; - } - - return nil; -} - -- (void)removeViewWithUUID:(NSInteger)uuid -{ - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if (view.entityUUID == uuid) - { - [view removeFromSuperview]; - - if (self.entityRemoved != nil) - self.entityRemoved(view); - break; - } - } -} - -- (void)removeAll -{ - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - [view removeFromSuperview]; - } -} - -- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer -{ - CGPoint location = [gestureRecognizer locationInView:self]; - - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateBegan: - { - if (_currentView != nil) - return; - - _currentView = [self viewForLocation:location]; - } - break; - - case UIGestureRecognizerStateChanged: - { - if (_currentView == nil) - return; - - CGFloat scale = gestureRecognizer.scale; - [_currentView scale:scale absolute:false]; - - [gestureRecognizer setScale:1.0f]; - } - break; - - case UIGestureRecognizerStateEnded: - { - _currentView = nil; - } - break; - - case UIGestureRecognizerStateCancelled: - { - _currentView = nil; - } - break; - - default: - break; - } -} - -- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer -{ - CGPoint location = [gestureRecognizer locationInView:self]; - - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateBegan: - { - if (_currentView != nil) - return; - - _currentView = [self viewForLocation:location]; - } - break; - - case UIGestureRecognizerStateChanged: - { - if (_currentView == nil) - return; - - CGFloat rotation = gestureRecognizer.rotation; - [_currentView rotate:rotation absolute:false]; - - [gestureRecognizer setRotation:0.0f]; - } - break; - - case UIGestureRecognizerStateEnded: - { - - } - break; - - case UIGestureRecognizerStateCancelled: - { - - } - break; - - default: - break; - } -} - -- (TGPhotoPaintEntityView *)viewForLocation:(CGPoint)__unused location -{ - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if (view.selectionView != nil) - return view; - } - - return nil; -} - -- (UIImage *)imageInRect:(CGRect)rect background:(UIImage *)background still:(bool)still -{ - if (self.subviews.count < 2) - return nil; - - UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), false, 1.0f); - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGRect bounds = CGRectMake(0, 0, rect.size.width, rect.size.height); - [background drawInRect:bounds]; - - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) - { - [self drawView:view inContext:context withBlock:^ - { - TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)view; - UIImage *image = stickerView.image; - if (image != nil) { - CGSize fittedSize = TGScaleToSize(image.size, view.bounds.size); - - CGContextTranslateCTM(context, view.bounds.size.width / 2.0f, view.bounds.size.height / 2.0f); - if (stickerView.isMirrored) - CGContextScaleCTM(context, -1, 1); - - [image drawInRect:CGRectMake(-fittedSize.width / 2.0f, -fittedSize.height / 2.0f, fittedSize.width, fittedSize.height)]; - } - }]; - } - else if ([view isKindOfClass:[TGPhotoTextEntityView class]]) - { - [self drawView:view inContext:context withBlock:^ - { - [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:false]; - }]; - } - } - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return image; -} - -- (void)drawView:(UIView *)view inContext:(CGContextRef)context withBlock:(void (^)(void))block -{ - CGContextSaveGState(context); - - CGContextTranslateCTM(context, view.center.x, view.center.y); - CGContextConcatCTM(context, view.transform); - CGContextTranslateCTM(context, -view.bounds.size.width / 2.0f, -view.bounds.size.height / 2.0f); - - block(); - - CGContextRestoreGState(context); -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - bool pointInside = [super pointInside:point withEvent:event]; - if (!pointInside) - { - for (UIView *subview in self.subviews) - { - CGPoint convertedPoint = [self convertPoint:point toView:subview]; - if ([subview pointInside:convertedPoint withEvent:event]) - pointInside = true; - } - } - return pointInside; -} - -- (bool)isTrackingAnyEntityView -{ - bool tracking = false; - for (TGPhotoPaintEntityView *view in self.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if (view.isTracking) - { - tracking = true; - break; - } - } - return tracking; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.h deleted file mode 100644 index 602c2fc6105..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.h +++ /dev/null @@ -1,15 +0,0 @@ -#import - -@interface TGPhotoPaintActionsView : UIView - -@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; - -@property (nonatomic, copy) void (^undoPressed)(void); -@property (nonatomic, copy) void (^redoPressed)(void); -@property (nonatomic, copy) void (^clearPressed)(UIView *); - -- (void)setUndoEnabled:(bool)enabled; -- (void)setRedoEnabled:(bool)enabled; -- (void)setClearEnabled:(bool)enabled; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.m deleted file mode 100644 index 8b3ca16860a..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintActionsView.m +++ /dev/null @@ -1,115 +0,0 @@ -#import "TGPhotoPaintActionsView.h" - -#import "LegacyComponentsInternal.h" -#import "TGFont.h" -#import "TGImageUtils.h" - -#import - -@interface TGPhotoPaintActionsView () -{ - TGModernButton *_undoButton; - TGModernButton *_redoButton; - TGModernButton *_clearButton; -} -@end - -@implementation TGPhotoPaintActionsView - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - _undoButton = [[TGModernButton alloc] init]; - _undoButton.adjustsImageWhenDisabled = false; - _undoButton.enabled = false; - _undoButton.exclusiveTouch = true; - [_undoButton setImage:TGTintedImage([UIImage imageNamed:@"Editor/Undo"], [UIColor whiteColor]) forState:UIControlStateNormal]; - [_undoButton addTarget:self action:@selector(undoButtonPressed) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:_undoButton]; - - _redoButton = [[TGModernButton alloc] init]; - _redoButton.adjustsImageWhenDisabled = false; - _redoButton.enabled = false; - _redoButton.exclusiveTouch = true; - [_redoButton setImage:TGComponentsImageNamed(@"PaintRedoIcon") forState:UIControlStateNormal]; - [_redoButton addTarget:self action:@selector(redoButtonPressed) forControlEvents:UIControlEventTouchUpInside]; - //[self addSubview:_redoButton]; - - _clearButton = [[TGModernButton alloc] init]; - _clearButton.enabled = false; - _clearButton.exclusiveTouch = true; - _clearButton.titleLabel.font = TGSystemFontOfSize(17.0f); - _clearButton.titleLabel.textAlignment = NSTextAlignmentCenter; - [_clearButton setTitle:TGLocalized(@"Paint.Clear") forState:UIControlStateNormal]; - [_clearButton setTitleColor:[UIColor whiteColor]]; - [_clearButton addTarget:self action:@selector(clearButtonPressed) forControlEvents:UIControlEventTouchUpInside]; - [_clearButton sizeToFit]; - [self addSubview:_clearButton]; - } - return self; -} - -- (void)undoButtonPressed -{ - if (self.undoPressed != nil) - self.undoPressed(); -} - -- (void)redoButtonPressed -{ - if (self.redoPressed != nil) - self.redoPressed(); -} - -- (void)clearButtonPressed -{ - if (self.clearPressed != nil) - self.clearPressed(_clearButton); -} - -- (void)setUndoEnabled:(bool)enabled -{ - _undoButton.enabled = enabled; -} - -- (void)setRedoEnabled:(bool)enabled -{ - _redoButton.enabled = enabled; -} - -- (void)setClearEnabled:(bool)enabled -{ - _clearButton.enabled = enabled; -} - -- (void)layoutSubviews -{ - if (self.frame.size.width > self.frame.size.height) - { - _undoButton.frame = CGRectMake(6, 0, 40, self.frame.size.height); - _redoButton.frame = CGRectMake(CGRectGetMaxX(_undoButton.frame) + 18, 0, 40, self.frame.size.height); - - _clearButton.titleLabel.font = TGSystemFontOfSize(17.0f); - _clearButton.titleLabel.numberOfLines = 1; - - if (_clearButton.frame.size.width < FLT_EPSILON) { - _clearButton.frame = CGRectMake(0, 0, 100, self.frame.size.height); - [_clearButton sizeToFit]; - } - - _clearButton.frame = CGRectMake(self.frame.size.width - _clearButton.frame.size.width - 10.0f, 0, _clearButton.frame.size.width, self.frame.size.height); - } - else - { - //_redoButton.frame = CGRectMake(0, self.frame.size.height - 40 - 14, self.frame.size.width, 40); - _undoButton.frame = CGRectMake(0, self.frame.size.height - 40 - 6, self.frame.size.width, 40); - - _clearButton.titleLabel.font = TGSystemFontOfSize(13.0f); - _clearButton.titleLabel.numberOfLines = 2; - _clearButton.frame = CGRectMake(0.0f, 10.0f, self.frame.size.width, 24.0f); - } -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.h b/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.h deleted file mode 100644 index 02a89beb1d6..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.h +++ /dev/null @@ -1,14 +0,0 @@ -#import - -@class TGPaintSwatch; - -@interface TGPhotoPaintColorPicker : UIControl - -@property (nonatomic, copy) void (^beganPicking)(void); -@property (nonatomic, copy) void (^valueChanged)(void); -@property (nonatomic, copy) void (^finishedPicking)(void); - -@property (nonatomic, strong) TGPaintSwatch *swatch; -@property (nonatomic, assign) UIInterfaceOrientation orientation; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.m b/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.m deleted file mode 100644 index 71812d67a34..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintColorPicker.m +++ /dev/null @@ -1,737 +0,0 @@ -#import "TGPhotoPaintColorPicker.h" - -#import "LegacyComponentsInternal.h" -#import "TGImageUtils.h" - -#import - -#import "TGPaintSwatch.h" - -const CGFloat TGPhotoPaintColorWeightGestureRange = 320.0f; -const CGFloat TGPhotoPaintVerticalThreshold = 5.0f; -const CGFloat TGPhotoPaintPreviewOffset = -70.0f; -const CGFloat TGPhotoPaintPreviewScale = 2.0f; -const CGFloat TGPhotoPaintDefaultBrushWeight = 0.08f; -const CGFloat TGPhotoPaintDefaultColorLocation = 0.0f; - -@interface TGPhotoPaintColorPickerKnobCircleView : UIView -{ - CGFloat _strokeIntensity; -} - -@property (nonatomic, strong) UIColor *color; -@property (nonatomic, assign) bool strokesLowContrastColors; - -@end - -@interface TGPhotoPaintColorPickerKnob : UIView -{ - UIView *_wrapperView; - UIImageView *_shadowView; - TGPhotoPaintColorPickerKnobCircleView *_backgroundView; - TGPhotoPaintColorPickerKnobCircleView *_colorView; - - bool _dragging; - CGFloat _weight; -} - -- (void)setColor:(UIColor *)color; -- (void)setWeight:(CGFloat)weight; - -@property (nonatomic, assign) bool dragging; -@property (nonatomic, assign) bool changingWeight; -@property (nonatomic, assign) UIInterfaceOrientation orientation; - -@end - -@interface TGPhotoPaintColorPickerBackground : UIView - -+ (NSArray *)colors; -+ (NSArray *)locations; - -@end - -@interface TGPhotoPaintColorPicker () -{ - TGPhotoPaintColorPickerBackground *_backgroundView; - TGPhotoPaintColorPickerKnob *_knobView; - - UIPanGestureRecognizer *_panGestureRecognizer; - UILongPressGestureRecognizer *_pressGestureRecognizer; - UITapGestureRecognizer *_tapGestureRecognizer; - - CGPoint _gestureStartLocation; - - CGFloat _location; - bool _dragging; - - CGFloat _weight; -} -@end - -@implementation TGPhotoPaintColorPicker - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - _backgroundView = [[TGPhotoPaintColorPickerBackground alloc] initWithFrame:self.bounds]; - _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self addSubview:_backgroundView]; - - _knobView = [[TGPhotoPaintColorPickerKnob alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 24.0f, 24.0f)]; - [_knobView setColor:[TGPhotoPaintColorPicker colorForLocation:0.0f]]; - [self addSubview:_knobView]; - - _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _panGestureRecognizer.delegate = self; - [self addGestureRecognizer:_panGestureRecognizer]; - - _pressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePress:)]; - _pressGestureRecognizer.delegate = self; - _pressGestureRecognizer.minimumPressDuration = 0.1; - [self addGestureRecognizer:_pressGestureRecognizer]; - - _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; - [self addGestureRecognizer:_tapGestureRecognizer]; - - _location = [self restoreLastColorLocation]; - _weight = TGPhotoPaintDefaultBrushWeight; - } - return self; -} - -- (CGFloat)restoreLastColorLocation -{ - NSNumber *lastColor = [[NSUserDefaults standardUserDefaults] objectForKey:@"TG_paintLastColorLocation_v0"]; - if (lastColor != nil) - return [lastColor floatValue]; - - return TGPhotoPaintDefaultColorLocation; -} - -- (void)storeCurrentColorLocation -{ - [[NSUserDefaults standardUserDefaults] setObject:@(_location) forKey:@"TG_paintLastColorLocation_v0"]; -} - -- (void)setOrientation:(UIInterfaceOrientation)orientation -{ - _orientation = orientation; - _knobView.orientation = orientation; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - if (CGRectContainsPoint(CGRectInset(self.bounds, -30.0f, -10.0f), point)) - return true; - - return [super pointInside:point withEvent:event]; -} - -- (TGPaintSwatch *)swatch -{ - return [TGPaintSwatch swatchWithColor:[TGPhotoPaintColorPicker colorForLocation:_location] colorLocation:_location brushWeight:_weight]; -} - -- (void)setSwatch:(TGPaintSwatch *)swatch -{ - [self setLocation:swatch.colorLocation]; - [self setWeight:swatch.brushWeight]; -} - -- (UIColor *)color -{ - return [TGPhotoPaintColorPicker colorForLocation:_location]; -} - -- (void)setLocation:(CGFloat)location -{ - [self setLocation:location animated:false]; -} - -- (void)setLocation:(CGFloat)location animated:(bool)animated -{ - _location = location; - [_knobView setColor:[TGPhotoPaintColorPicker colorForLocation:_location]]; - - if (animated) - { - [UIView animateWithDuration:0.3 delay:0.0 usingSpringWithDamping:0.85f initialSpringVelocity:0.0f options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionLayoutSubviews animations:^ - { - [self layoutSubviews]; - } completion:nil]; - } - else - { - [self setNeedsLayout]; - } -} - -- (void)setWeight:(CGFloat)weight -{ - _weight = weight; - [_knobView setWeight:weight]; -} - -- (void)setDragging:(bool)dragging -{ - _dragging = dragging; - [_knobView setDragging:dragging]; -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer -{ - return true; -} - -- (bool)_hasBeganChangingWeight:(CGPoint)location -{ - switch (self.orientation) - { - case UIInterfaceOrientationLandscapeLeft: - if (location.x > self.frame.size.width + TGPhotoPaintVerticalThreshold) - return true; - - case UIInterfaceOrientationLandscapeRight: - if (location.x < -TGPhotoPaintVerticalThreshold) - return true; - - default: - if (location.y < -TGPhotoPaintVerticalThreshold) - return true; - } - - return false; -} - -- (CGFloat)_weightLocation:(CGPoint)location -{ - CGFloat weightLocation = 0.0f; - switch (self.orientation) - { - case UIInterfaceOrientationLandscapeLeft: - weightLocation = (location.x - self.frame.size.width - TGPhotoPaintVerticalThreshold) / TGPhotoPaintColorWeightGestureRange; - break; - - case UIInterfaceOrientationLandscapeRight: - weightLocation = ((location.x * -1) - TGPhotoPaintVerticalThreshold) / TGPhotoPaintColorWeightGestureRange; - break; - - default: - weightLocation = ((location.y * -1) - TGPhotoPaintVerticalThreshold) / TGPhotoPaintColorWeightGestureRange; - break; - } - - return MAX(0.0f, MIN(1.0f, weightLocation)); -} - -- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer -{ - CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view]; - - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateBegan: - { - _gestureStartLocation = location; - [self setDragging:true]; - - if (self.beganPicking != nil) - self.beganPicking(); - } - break; - - case UIGestureRecognizerStateChanged: - { - CGFloat colorLocation = MAX(0.0f, MIN(1.0f, self.frame.size.width > self.frame.size.height ? location.x / gestureRecognizer.view.frame.size.width : location.y / gestureRecognizer.view.frame.size.height)); - [self setLocation:colorLocation]; - - if ([self _hasBeganChangingWeight:location]) - { - [_knobView setChangingWeight:true]; - CGFloat weightLocation = [self _weightLocation:location]; - [self setWeight:weightLocation]; - } - - if (self.valueChanged != nil) - self.valueChanged(); - } - break; - - case UIGestureRecognizerStateEnded: - { - [_knobView setChangingWeight:false]; - [self setDragging:false]; - - if (self.finishedPicking != nil) - self.finishedPicking(); - - [self storeCurrentColorLocation]; - } - break; - - case UIGestureRecognizerStateCancelled: - { - [_knobView setChangingWeight:false]; - } - break; - - default: - break; - } -} - -- (void)handlePress:(UILongPressGestureRecognizer *)gestureRecognizer -{ - CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view]; - - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateBegan: - { - if (!CGRectContainsPoint(_knobView.frame, location)) - { - CGFloat colorLocation = MAX(0.0f, MIN(1.0f, self.frame.size.width > self.frame.size.height ? location.x / gestureRecognizer.view.frame.size.width : location.y / gestureRecognizer.view.frame.size.height)); - [self setLocation:colorLocation animated:true]; - } - - [self setDragging:true]; - - if (self.beganPicking != nil) - self.beganPicking(); - } - break; - - case UIGestureRecognizerStateEnded: - { - [self setDragging:false]; - - if (self.finishedPicking != nil) - self.finishedPicking(); - - [self storeCurrentColorLocation]; - } - break; - - default: - break; - } -} - -- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer -{ - CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view]; - if (!CGRectContainsPoint(_knobView.frame, location)) - { - CGFloat colorLocation = MAX(0.0f, MIN(1.0f, self.frame.size.width > self.frame.size.height ? location.x / gestureRecognizer.view.frame.size.width : location.y / gestureRecognizer.view.frame.size.height)); - [self setLocation:colorLocation animated:true]; - - if (self.finishedPicking != nil) - self.finishedPicking(); - - [self storeCurrentColorLocation]; - } -} - -+ (UIColor *)colorForLocation:(CGFloat)location -{ - NSArray *locations = [TGPhotoPaintColorPickerBackground locations]; - NSArray *colors = [TGPhotoPaintColorPickerBackground colors]; - - if (location < FLT_EPSILON) - return [UIColor colorWithCGColor:(CGColorRef)colors.firstObject]; - else if (location > 1 - FLT_EPSILON) - return [UIColor colorWithCGColor:(CGColorRef)colors.lastObject]; - - __block NSInteger leftIndex = -1; - __block NSInteger rightIndex = -1; - - [locations enumerateObjectsUsingBlock:^(NSNumber *value, NSUInteger index, BOOL *stop) - { - if (index > 0) - { - if (value.doubleValue > location) - { - leftIndex = index - 1; - rightIndex = index; - *stop = true; - } - } - }]; - - CGFloat leftLocation = [locations[leftIndex] doubleValue]; - UIColor *leftColor = [UIColor colorWithCGColor:(CGColorRef)colors[leftIndex]]; - - CGFloat rightLocation = [locations[rightIndex] doubleValue]; - UIColor *rightColor = [UIColor colorWithCGColor:(CGColorRef)colors[rightIndex]]; - - CGFloat factor = (location - leftLocation) / (rightLocation - leftLocation); - return [self _interpolateColor:leftColor withColor:rightColor factor:factor]; -} - -+ (void)_colorComponentsForColor:(UIColor *)color red:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue -{ - NSInteger componentsCount = CGColorGetNumberOfComponents(color.CGColor); - const CGFloat *components = CGColorGetComponents(color.CGColor); - - CGFloat r = 0.0f; - CGFloat g = 0.0f; - CGFloat b = 0.0f; - CGFloat a = 1.0f; - - if (componentsCount == 4) - { - r = components[0]; - g = components[1]; - b = components[2]; - a = components[3]; - } - else - { - r = g = b = components[0]; - } - - *red = r; - *green = g; - *blue = b; -} - -+ (UIColor *)_interpolateColor:(UIColor *)color1 withColor:(UIColor *)color2 factor:(CGFloat)factor -{ - factor = MIN(MAX(factor, 0.0), 1.0); - - CGFloat r1 = 0, r2 = 0; - CGFloat g1 = 0, g2 = 0; - CGFloat b1 = 0, b2 = 0; - - [self _colorComponentsForColor:color1 red:&r1 green:&g1 blue:&b1]; - [self _colorComponentsForColor:color2 red:&r2 green:&g2 blue:&b2]; - - CGFloat r = r1 + (r2 - r1) * factor; - CGFloat g = g1 + (g2 - g1) * factor; - CGFloat b = b1 + (b2 - b1) * factor; - - return [UIColor colorWithRed:r green:g blue:b alpha:1.0f]; -} - -- (void)layoutSubviews -{ - CGFloat pos = self.frame.size.width > self.frame.size.height ? -_knobView.frame.size.width / 2.0f + self.frame.size.width * _location : -_knobView.frame.size.height / 2.0f + self.frame.size.height * _location; - - _knobView.frame = self.frame.size.width > self.frame.size.height ? CGRectMake(pos, (self.frame.size.height - _knobView.frame.size.height) / 2.0f, _knobView.frame.size.width, _knobView.frame.size.height) : CGRectMake((self.frame.size.width - _knobView.frame.size.width) / 2.0f, pos, _knobView.frame.size.width, _knobView.frame.size.height); -} - -@end - -@implementation TGPhotoPaintColorPickerKnobCircleView - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.backgroundColor = [UIColor clearColor]; - self.contentMode = UIViewContentModeRedraw; - self.opaque = false; - } - return self; -} - -- (void)setColor:(UIColor *)color -{ - _color = color; - - if (self.strokesLowContrastColors) - { - CGFloat strokeIntensity = 0.0f; - CGFloat hue; - CGFloat saturation; - CGFloat brightness; - CGFloat alpha; - - bool success = [color getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; - if (success && hue < FLT_EPSILON && saturation < FLT_EPSILON && brightness > 0.92f) - strokeIntensity = (brightness - 0.92f) / 0.08f; - - _strokeIntensity = strokeIntensity; - } - - [self setNeedsDisplay]; -} - -- (void)drawRect:(CGRect)rect -{ - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGContextSetFillColorWithColor(context, self.color.CGColor); - CGContextFillEllipseInRect(context, rect); - - if (_strokeIntensity > FLT_EPSILON) - { - CGContextSetLineWidth(context, 1.0f); - CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.88f alpha:_strokeIntensity].CGColor); - CGContextStrokeEllipseInRect(context, CGRectInset(rect, 1.0f, 1.0f)); - } -} - -@end - - -const CGFloat TGPhotoPaintColorSmallCircle = 4.0f; -const CGFloat TGPhotoPaintColorLargeCircle = 20.0f; - -@implementation TGPhotoPaintColorPickerKnob - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.userInteractionEnabled = false; - - _wrapperView = [[UIView alloc] initWithFrame:self.bounds]; - [self addSubview:_wrapperView]; - - static UIImage *shadowImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^ - { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(48.0f, 48.0f), false, 0.0f); - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGContextSetShadowWithColor(context, CGSizeMake(0, 1.5f), 4.0f, [UIColor colorWithWhite:0.0f alpha:0.7f].CGColor); - CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 1.0f).CGColor); - CGContextFillEllipseInRect(context, CGRectMake(4.0f, 4.0f, 40.0f, 40.0f)); - shadowImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - }); - - _shadowView = [[UIImageView alloc] initWithImage:shadowImage]; - _shadowView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _shadowView.frame = CGRectInset(_wrapperView.bounds, -2, -2); - [_wrapperView addSubview:_shadowView]; - - _backgroundView = [[TGPhotoPaintColorPickerKnobCircleView alloc] initWithFrame:self.bounds]; - _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _backgroundView.color = [UIColor whiteColor]; - [_wrapperView addSubview:_backgroundView]; - - _colorView = [[TGPhotoPaintColorPickerKnobCircleView alloc] init]; - _colorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - _colorView.center = TGPaintCenterOfRect(self.bounds); - _colorView.color = [UIColor blueColor]; - _colorView.strokesLowContrastColors = true; - [self setWeight:0.5f]; - [_wrapperView addSubview:_colorView]; - } - return self; -} - -- (void)setColor:(UIColor *)color -{ - [_colorView setColor:color]; -} - -- (void)setWeight:(CGFloat)size -{ - _weight = size; - if (_dragging) - [self updateLocationAnimated:true updateColorSize:false]; - - CGFloat diameter = [self _circleDiameterForBrushWeight:size zoomed:_dragging]; - [_colorView setBounds:CGRectMake(0, 0, diameter, diameter)]; -} - -- (void)setDragging:(bool)dragging -{ - if (dragging == _dragging) - return; - - _dragging = dragging; - [self updateLocationAnimated:true updateColorSize:true]; -} - -- (CGFloat)_circleDiameterForBrushWeight:(CGFloat)size zoomed:(bool)zoomed -{ - CGFloat result = TGPhotoPaintColorSmallCircle + (TGPhotoPaintColorLargeCircle - TGPhotoPaintColorSmallCircle) * size; - result = zoomed ? TGRetinaFloor(result) * TGPhotoPaintPreviewScale : floor(result); - return result; -} - -- (void)updateLocationAnimated:(bool)animated updateColorSize:(bool)updateColorSize -{ - void (^changeBlock)(void) = ^ - { - CGPoint center = TGPaintCenterOfRect(self.bounds); - CGFloat scale = 1.0f; - if (_dragging) - { - scale = TGPhotoPaintPreviewScale; - - CGFloat offset = TGPhotoPaintPreviewOffset; - if (self.changingWeight) - offset -= _weight * TGPhotoPaintColorWeightGestureRange; - - switch (self.orientation) - { - case UIInterfaceOrientationLandscapeLeft: - center.x -= offset; - break; - - case UIInterfaceOrientationLandscapeRight: - center.x += offset; - break; - - default: - center.y += offset; - break; - } - } - - _wrapperView.center = center; - _wrapperView.bounds = CGRectMake(0, 0, 24.0f * scale, 24.0f * scale); - - if (updateColorSize) - { - CGFloat diameter = [self _circleDiameterForBrushWeight:_weight zoomed:_dragging]; - [_colorView setBounds:CGRectMake(0, 0, diameter, diameter)]; - } - }; - - if (animated) - { - [UIView animateWithDuration:0.3 delay:0.0 usingSpringWithDamping:0.85f initialSpringVelocity:0.0f options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionLayoutSubviews animations:changeBlock completion:nil]; - } - else - { - changeBlock(); - } -} - -@end - - -static void addRoundedRectToPath(CGContextRef context, CGRect rect, CGFloat ovalWidth, CGFloat ovalHeight) -{ - CGFloat fw, fh; - if (ovalWidth == 0 || ovalHeight == 0) - { - CGContextAddRect(context, rect); - return; - } - - CGContextSaveGState(context); - CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect)); - CGContextScaleCTM (context, ovalWidth, ovalHeight); - fw = CGRectGetWidth (rect) / ovalWidth; - fh = CGRectGetHeight (rect) / ovalHeight; - CGContextMoveToPoint(context, fw, fh/2); - CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1); - CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1); - CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1); - CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1); - CGContextClosePath(context); - CGContextRestoreGState(context); -} - -@implementation TGPhotoPaintColorPickerBackground - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.backgroundColor = [UIColor clearColor]; - self.contentMode = UIViewContentModeRedraw; - self.opaque = false; - } - return self; -} - -- (void)drawRect:(CGRect)rect -{ - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGFloat radius = rect.size.width > rect.size.height ? rect.size.height / 2.0f : rect.size.width / 2.0f; - addRoundedRectToPath(context, self.frame, radius, radius); - CGContextClip(context); - - CFArrayRef colors = (__bridge CFArrayRef)[TGPhotoPaintColorPickerBackground colors]; - CGFloat locations[[TGPhotoPaintColorPickerBackground colors].count]; - [TGPhotoPaintColorPickerBackground fillLocations:locations]; - - CGColorSpaceRef colorSpc = CGColorSpaceCreateDeviceRGB(); - CGGradientRef gradient = CGGradientCreateWithColors(colorSpc, colors, locations); - - if (rect.size.width > rect.size.height) - { - CGContextDrawLinearGradient(context, gradient, CGPointMake(0.0f, rect.size.height / 2.0f), CGPointMake(rect.size.width, rect.size.height / 2.0f), kCGGradientDrawsAfterEndLocation); - } - else - { - CGContextDrawLinearGradient(context, gradient, CGPointMake(rect.size.width / 2.0f, 0.0f), CGPointMake(rect.size.width / 2.0f, rect.size.height), kCGGradientDrawsAfterEndLocation); - } - - CGContextSetBlendMode(context, kCGBlendModeClear); - CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor); - - CGColorSpaceRelease(colorSpc); - CGGradientRelease(gradient); -} - -+ (NSArray *)colors -{ - static dispatch_once_t onceToken; - static NSArray *colors; - dispatch_once(&onceToken, ^ - { - colors = @ - [ - (id)UIColorRGB(0xea2739).CGColor, //red - (id)UIColorRGB(0xdb3ad2).CGColor, //pink - (id)UIColorRGB(0x3051e3).CGColor, //blue - (id)UIColorRGB(0x49c5ed).CGColor, //cyan - (id)UIColorRGB(0x80c864).CGColor, //green - (id)UIColorRGB(0xfcde65).CGColor, //yellow - (id)UIColorRGB(0xfc964d).CGColor, //orange - (id)UIColorRGB(0x000000).CGColor, //black - (id)UIColorRGB(0xffffff).CGColor //white - ]; - }); - return colors; -} - -+ (NSArray *)locations -{ - static dispatch_once_t onceToken; - static NSArray *locations; - dispatch_once(&onceToken, ^ - { - locations = @ - [ - @0.0f, //red - @0.14f, //pink - @0.24f, //blue - @0.39f, //cyan - @0.49f, //green - @0.62f, //yellow - @0.73f, //orange - @0.85f, //black - @1.0f //white - ]; - }); - return locations; -} - -+ (void)fillLocations:(CGFloat *)buf -{ - NSArray *locations = [self locations]; - [locations enumerateObjectsUsingBlock:^(NSNumber *location, NSUInteger index, __unused BOOL *stop) - { - buf[index] = location.doubleValue; - }]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m b/submodules/LegacyComponents/Sources/TGPhotoPaintController.m deleted file mode 100644 index 7d882e38f31..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintController.m +++ /dev/null @@ -1,2634 +0,0 @@ -#import "TGPhotoPaintController.h" - -#import "LegacyComponentsInternal.h" - -#import - -#import -#import -#import -#import "TGPhotoEditorInterfaceAssets.h" -#import - -#import -#import - -#import "TGMenuSheetController.h" - -#import -#import - -#import "TGPainting.h" -#import -#import "TGPaintRadialBrush.h" -#import "TGPaintEllipticalBrush.h" -#import "TGPaintNeonBrush.h" -#import "TGPaintArrowBrush.h" -#import "TGPaintCanvas.h" -#import "TGPaintingWrapperView.h" -#import "TGPaintState.h" -#import "TGPaintBrushPreview.h" -#import "TGPaintSwatch.h" -#import "TGPhotoPaintFont.h" -#import - -#import "PGPhotoEditor.h" -#import "TGPhotoEditorPreviewView.h" - -#import "TGPhotoPaintActionsView.h" -#import "TGPhotoPaintSettingsView.h" - -#import "TGPhotoPaintSettingsWrapperView.h" -#import "TGPhotoBrushSettingsView.h" -#import "TGPhotoTextSettingsView.h" - -#import "TGPhotoPaintSelectionContainerView.h" -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoStickerEntityView.h" -#import "TGPhotoTextEntityView.h" -#import "TGPhotoPaintEyedropperView.h" - -#import "TGPaintFaceDetector.h" -#import "TGPhotoMaskPosition.h" - -const CGFloat TGPhotoPaintTopPanelSize = 44.0f; -const CGFloat TGPhotoPaintBottomPanelSize = 79.0f; -const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f }; -const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f }; - -const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f; - -@interface TGPhotoPaintController () -{ - TGPaintUndoManager *_undoManager; - TGObserverProxy *_keyboardWillChangeFrameProxy; - CGFloat _keyboardHeight; - - TGModernGalleryZoomableScrollView *_scrollView; - UIView *_scrollContentView; - - UIButton *_containerView; - TGPhotoEditorSparseView *_wrapperView; - UIView *_portraitToolsWrapperView; - UIView *_landscapeToolsWrapperView; - - UIPinchGestureRecognizer *_pinchGestureRecognizer; - UIRotationGestureRecognizer *_rotationGestureRecognizer; - - NSArray *_brushes; - TGPainting *_painting; - TGPaintCanvas *_canvasView; - TGPaintBrushPreview *_brushPreview; - - CGSize _previousSize; - UIView *_contentView; - UIView *_contentWrapperView; - - UIView *_dimView; - TGModernButton *_doneButton; - - TGPhotoPaintActionsView *_landscapeActionsView; - TGPhotoPaintActionsView *_portraitActionsView; - - TGPhotoPaintSettingsView *_portraitSettingsView; - TGPhotoPaintSettingsView *_landscapeSettingsView; - - TGPhotoPaintSettingsWrapperView *_settingsViewWrapper; - UIView *_settingsView; - id _stickersScreen; - - double _stickerStartTime; - - bool _appeared; - bool _skipEntitiesSetup; - bool _entitiesReady; - - TGPhotoPaintFont *_selectedTextFont; - TGPhotoPaintTextEntityStyle _selectedTextStyle; - - TGPhotoEntitiesContainerView *_entitiesContainerView; - TGPhotoPaintEntityView *_currentEntityView; - - TGPhotoPaintSelectionContainerView *_selectionContainerView; - TGPhotoPaintEntitySelectionView *_entitySelectionView; - TGPhotoPaintEyedropperView *_eyedropperView; - - TGPhotoTextEntityView *_editedTextView; - CGPoint _editedTextCenter; - CGAffineTransform _editedTextTransform; - UIButton *_textEditingDismissButton; - - TGMenuContainerView *_menuContainerView; - - TGPaintingData *_resultData; - - TGPaintingWrapperView *_paintingWrapperView; - - bool _enableStickers; - - NSData *_eyedropperBackgroundData; - CGSize _eyedropperBackgroundSize; - NSInteger _eyedropperBackgroundBytesPerRow; - CGBitmapInfo _eyedropperBackgroundInfo; - - id _context; -} - -@property (nonatomic, strong) ASHandle *actionHandle; - -@property (nonatomic, weak) PGPhotoEditor *photoEditor; -@property (nonatomic, weak) TGPhotoEditorPreviewView *previewView; - -@end - -@implementation TGPhotoPaintController - -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView -{ - self = [super initWithContext:context]; - if (self != nil) - { - _context = context; - _enableStickers = photoEditor.enableStickers; - - _stickerStartTime = NAN; - - _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; - - self.photoEditor = photoEditor; - self.previewView = previewView; - _entitiesContainerView = entitiesView; - if (entitiesView != nil) { - _skipEntitiesSetup = true; - } - entitiesView.userInteractionEnabled = true; - - _brushes = @ - [ - [[TGPaintRadialBrush alloc] init], - [[TGPaintEllipticalBrush alloc] init], - [[TGPaintNeonBrush alloc] init], - [[TGPaintArrowBrush alloc] init], - ]; - _selectedTextFont = [[TGPhotoPaintFont availableFonts] firstObject]; - _selectedTextStyle = TGPhotoPaintTextEntityStyleFramed; - - if (_photoEditor.paintingData.undoManager != nil) - _undoManager = [_photoEditor.paintingData.undoManager copy]; - else - _undoManager = [[TGPaintUndoManager alloc] init]; - - CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]); - _painting = [[TGPainting alloc] initWithSize:size undoManager:_undoManager imageData:[_photoEditor.paintingData data]]; - _undoManager.painting = _painting; - - _keyboardWillChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification]; - } - return self; -} - -- (void)dealloc -{ - [_actionHandle reset]; -} - -- (void)loadView -{ - [super loadView]; - self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - - _scrollView = [[TGModernGalleryZoomableScrollView alloc] initWithFrame:self.view.bounds hasDoubleTap:false]; - if (@available(iOS 11.0, *)) { - _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } - _scrollView.contentInset = UIEdgeInsetsZero; - _scrollView.delegate = self; - _scrollView.showsHorizontalScrollIndicator = false; - _scrollView.showsVerticalScrollIndicator = false; - [self.view addSubview:_scrollView]; - - _scrollContentView = [[UIView alloc] initWithFrame:self.view.bounds]; - [_scrollView addSubview:_scrollContentView]; - - _containerView = [[UIButton alloc] initWithFrame:self.view.bounds]; - _containerView.clipsToBounds = true; - [_containerView addTarget:self action:@selector(containerPressed) forControlEvents:UIControlEventTouchUpInside]; - [_scrollContentView addSubview:_containerView]; - - _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)]; - _pinchGestureRecognizer.delegate = self; - [_containerView addGestureRecognizer:_pinchGestureRecognizer]; - - _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotate:)]; - _rotationGestureRecognizer.delegate = self; - [_containerView addGestureRecognizer:_rotationGestureRecognizer]; - - TGPhotoEditorPreviewView *previewView = _previewView; - previewView.userInteractionEnabled = false; - previewView.hidden = true; - - __weak TGPhotoPaintController *weakSelf = self; - _paintingWrapperView = [[TGPaintingWrapperView alloc] init]; - _paintingWrapperView.clipsToBounds = true; - _paintingWrapperView.shouldReceiveTouch = ^bool - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - return (strongSelf->_editedTextView == nil); - }; - [_containerView addSubview:_paintingWrapperView]; - - _contentView = [[UIView alloc] init]; - _contentView.clipsToBounds = true; - _contentView.userInteractionEnabled = false; - [_containerView addSubview:_contentView]; - - _contentWrapperView = [[UIView alloc] init]; - _contentWrapperView.userInteractionEnabled = false; - [_contentView addSubview:_contentWrapperView]; - - if (_entitiesContainerView == nil) { - _entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init]; - _entitiesContainerView.clipsToBounds = true; - _entitiesContainerView.stickersContext = _stickersContext; - } - _entitiesContainerView.entitySelected = ^(TGPhotoPaintEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf selectEntityView:sender]; - }; - _entitiesContainerView.entityRemoved = ^(TGPhotoPaintEntityView *entity) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if (entity == strongSelf->_currentEntityView) - [strongSelf _clearCurrentSelection]; - - [strongSelf updateSettingsButton]; - }; - if (!_skipEntitiesSetup) { - [_contentWrapperView addSubview:_entitiesContainerView]; - } - _undoManager.entitiesContainer = _entitiesContainerView; - - _dimView = [[UIView alloc] init]; - _dimView.alpha = 0.0f; - _dimView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _dimView.backgroundColor = UIColorRGBA(0x000000, 0.4f); - _dimView.userInteractionEnabled = false; - [_entitiesContainerView addSubview:_dimView]; - - _selectionContainerView = [[TGPhotoPaintSelectionContainerView alloc] init]; - _selectionContainerView.clipsToBounds = false; - [_containerView addSubview:_selectionContainerView]; - - _eyedropperView = [[TGPhotoPaintEyedropperView alloc] init]; - _eyedropperView.locationChanged = ^(CGPoint location, bool finished) { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - { - UIColor *color = [strongSelf colorAtPoint:location]; - strongSelf->_eyedropperView.color = color; - - if (finished) { - TGPaintSwatch *swatch = [TGPaintSwatch swatchWithColor:color colorLocation:0.5 brushWeight:strongSelf->_portraitSettingsView.swatch.brushWeight]; - [strongSelf setCurrentSwatch:swatch sender:nil]; - - [strongSelf commitEyedropper:false]; - } - } - }; - _eyedropperView.hidden = true; - [_selectionContainerView addSubview:_eyedropperView]; - - _wrapperView = [[TGPhotoEditorSparseView alloc] initWithFrame:CGRectZero]; - [self.view addSubview:_wrapperView]; - - _portraitToolsWrapperView = [[UIView alloc] initWithFrame:CGRectZero]; - _portraitToolsWrapperView.alpha = 0.0f; - [_wrapperView addSubview:_portraitToolsWrapperView]; - - _landscapeToolsWrapperView = [[UIView alloc] initWithFrame:CGRectZero]; - _landscapeToolsWrapperView.alpha = 0.0f; - [_wrapperView addSubview:_landscapeToolsWrapperView]; - - void (^undoPressed)(void) = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf->_undoManager undo]; - }; - - void (^clearPressed)(UIView *) = ^(UIView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf presentClearAllAlert:sender]; - }; - - _portraitActionsView = [[TGPhotoPaintActionsView alloc] init]; - _portraitActionsView.alpha = 0.0f; - _portraitActionsView.undoPressed = undoPressed; - _portraitActionsView.clearPressed = clearPressed; - [_wrapperView addSubview:_portraitActionsView]; - - _landscapeActionsView = [[TGPhotoPaintActionsView alloc] init]; - _landscapeActionsView.alpha = 0.0f; - _landscapeActionsView.undoPressed = undoPressed; - _landscapeActionsView.clearPressed = clearPressed; - [_wrapperView addSubview:_landscapeActionsView]; - - _doneButton = [[TGModernButton alloc] init]; - _doneButton.alpha = 0.0f; - _doneButton.userInteractionEnabled = false; - [_doneButton setTitle:TGLocalized(@"Common.Done") forState:UIControlStateNormal]; - _doneButton.titleLabel.font = TGSystemFontOfSize(17.0); - [_doneButton sizeToFit]; -// [_wrapperView addSubview:_doneButton]; - - void (^settingsPressed)(void) = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf commitEyedropper:true]; - - if ([strongSelf->_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [strongSelf presentTextSettingsView]; - else if ([strongSelf->_currentEntityView isKindOfClass:[TGPhotoStickerEntityView class]]) - [strongSelf mirrorSelectedStickerEntity]; - else - [strongSelf presentBrushSettingsView]; - }; - - void (^eyedropperPressed)(void) = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [self enableEyedropper]; - }; - - void (^beganColorPicking)(void) = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf commitEyedropper:true]; - - if (![strongSelf->_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [strongSelf setDimHidden:false animated:true]; - }; - - void (^changedColor)(TGPhotoPaintSettingsView *, TGPaintSwatch *) = ^(TGPhotoPaintSettingsView *sender, TGPaintSwatch *swatch) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf setCurrentSwatch:swatch sender:sender]; - }; - - void (^finishedColorPicking)(TGPhotoPaintSettingsView *, TGPaintSwatch *) = ^(TGPhotoPaintSettingsView *sender, TGPaintSwatch *swatch) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf commitEyedropper:true]; - - [strongSelf setCurrentSwatch:swatch sender:sender]; - - if (![strongSelf->_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [strongSelf setDimHidden:true animated:true]; - }; - - _portraitSettingsView = [[TGPhotoPaintSettingsView alloc] initWithContext:_context]; - _portraitSettingsView.eyedropperPressed = eyedropperPressed; - _portraitSettingsView.beganColorPicking = beganColorPicking; - _portraitSettingsView.changedColor = changedColor; - _portraitSettingsView.finishedColorPicking = finishedColorPicking; - _portraitSettingsView.settingsPressed = settingsPressed; - _portraitSettingsView.layer.rasterizationScale = TGScreenScaling(); - _portraitSettingsView.interfaceOrientation = UIInterfaceOrientationPortrait; - [_portraitToolsWrapperView addSubview:_portraitSettingsView]; - - _landscapeSettingsView = [[TGPhotoPaintSettingsView alloc] initWithContext:_context]; - _landscapeSettingsView.eyedropperPressed = eyedropperPressed; - _landscapeSettingsView.beganColorPicking = beganColorPicking; - _landscapeSettingsView.changedColor = changedColor; - _landscapeSettingsView.finishedColorPicking = finishedColorPicking; - _landscapeSettingsView.settingsPressed = settingsPressed; - _landscapeSettingsView.layer.rasterizationScale = TGScreenScaling(); - _landscapeSettingsView.interfaceOrientation = UIInterfaceOrientationLandscapeLeft; - [_landscapeToolsWrapperView addSubview:_landscapeSettingsView]; - - [self setCurrentSwatch:_portraitSettingsView.swatch sender:nil]; - - if (![self _updateControllerInset:false]) - [self controllerInsetUpdated:UIEdgeInsetsZero]; -} - -- (void)setStickersContext:(id)stickersContext { - _stickersContext = stickersContext; - _entitiesContainerView.stickersContext = stickersContext; -} - -- (void)setupCanvas -{ - if (_canvasView == nil) { - __weak TGPhotoPaintController *weakSelf = self; - _canvasView = [[TGPaintCanvas alloc] initWithFrame:CGRectZero]; - _canvasView.pointInsideContainer = ^bool(CGPoint point) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - return [strongSelf->_containerView pointInside:[strongSelf->_canvasView convertPoint:point toView:strongSelf->_containerView] withEvent:nil]; - }; - _canvasView.shouldDraw = ^bool - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - return ![strongSelf->_entitiesContainerView isTrackingAnyEntityView]; - }; - _canvasView.shouldDrawOnSingleTap = ^bool - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - bool rotating = (strongSelf->_rotationGestureRecognizer.state == UIGestureRecognizerStateBegan || strongSelf->_rotationGestureRecognizer.state == UIGestureRecognizerStateChanged); - bool pinching = (strongSelf->_pinchGestureRecognizer.state == UIGestureRecognizerStateBegan || strongSelf->_pinchGestureRecognizer.state == UIGestureRecognizerStateChanged); - - if (strongSelf->_currentEntityView != nil && !rotating && !pinching) - { - [strongSelf selectEntityView:nil]; - return false; - } - - return true; - }; - _canvasView.strokeBegan = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf selectEntityView:nil]; - }; - _canvasView.strokeCommited = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf updateActionsView]; - }; - _canvasView.hitTest = ^UIView *(CGPoint point, UIEvent *event) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return nil; - - return [strongSelf->_entitiesContainerView hitTest:[strongSelf->_canvasView convertPoint:point toView:strongSelf->_entitiesContainerView] withEvent:event]; - }; - _canvasView.cropRect = _photoEditor.cropRect; - _canvasView.cropOrientation = _photoEditor.cropOrientation; - _canvasView.originalSize = _photoEditor.originalSize; - [_canvasView setPainting:_painting]; - [_canvasView setBrush:_brushes.firstObject]; - [self setCurrentSwatch:_portraitSettingsView.swatch sender:nil]; - [_paintingWrapperView addSubview:_canvasView]; - } - - _canvasView.hidden = false; - [self.view setNeedsLayout]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - PGPhotoEditor *photoEditor = _photoEditor; - if (!_skipEntitiesSetup) { - [_entitiesContainerView setupWithPaintingData:photoEditor.paintingData]; - } - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - [self _commonEntityViewSetup:view]; - } - - __weak TGPhotoPaintController *weakSelf = self; - _undoManager.historyChanged = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf updateActionsView]; - }; - - [self updateActionsView]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - [self transitionIn]; -} - -#pragma mark - Tab Bar - -- (TGPhotoEditorTab)availableTabs -{ - TGPhotoEditorTab result = TGPhotoEditorPaintTab | TGPhotoEditorEraserTab | TGPhotoEditorTextTab; - if (_enableStickers && _stickersContext != nil) { - result |= TGPhotoEditorStickerTab; - } - return result; -} - -- (void)handleTabAction:(TGPhotoEditorTab)tab -{ - [self commitEyedropper:true]; - - switch (tab) - { - case TGPhotoEditorStickerTab: - { - [self presentStickersView]; - } - break; - - case TGPhotoEditorTextTab: - { - [self createNewTextLabel]; - } - break; - - case TGPhotoEditorPaintTab: - { - [self selectEntityView:nil]; - - if (_canvasView.state.eraser) - [self toggleEraserMode]; - } - break; - - case TGPhotoEditorEraserTab: - { - [self selectEntityView:nil]; - [self toggleEraserMode]; - } - break; - - default: - break; - } -} - -- (TGPhotoEditorTab)activeTab -{ - TGPhotoEditorTab tabs = TGPhotoEditorNoneTab; - - if (_currentEntityView != nil) - return tabs; - - if (_canvasView.state.eraser) - tabs |= TGPhotoEditorEraserTab; - else - tabs |= TGPhotoEditorPaintTab; - - return tabs; -} - -#pragma mark - Undo & Redo - -- (void)updateActionsView -{ - if (_portraitActionsView == nil || _landscapeActionsView == nil) - return; - - NSArray *views = @[ _portraitActionsView, _landscapeActionsView ]; - for (TGPhotoPaintActionsView *view in views) - { - [view setUndoEnabled:_undoManager.canUndo]; - [view setClearEnabled:_undoManager.canUndo]; - } -} - -- (void)presentClearAllAlert:(UIView *)sender -{ - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.narrowInLandscape = true; - controller.permittedArrowDirections = UIPopoverArrowDirectionUp; - __weak TGMenuSheetController *weakController = controller; - - __weak TGPhotoPaintController *weakSelf = self; - NSArray *items = @ - [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Paint.ClearConfirm") type:TGMenuSheetButtonTypeDestructive fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController == nil) - return; - - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf->_painting clear]; - [strongSelf->_undoManager reset]; - - [strongSelf->_entitiesContainerView removeAll]; - [strongSelf _clearCurrentSelection]; - - [strongSelf updateSettingsButton]; - - [strongController dismissAnimated:true manual:false completion:nil]; - }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }] - ]; - - [controller setItemViews:items]; - controller.sourceRect = ^ - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return CGRectZero; - return [sender convertRect:sender.bounds toView:strongSelf.view]; - }; - [controller presentInViewController:self.parentViewController sourceView:self.view animated:true]; -} - -- (void)_clearCurrentSelection -{ - _scrollView.pinchGestureRecognizer.enabled = true; - _currentEntityView = nil; - if (_entitySelectionView != nil) - { - [_entitySelectionView removeFromSuperview]; - _entitySelectionView = nil; - } -} - -#pragma mark - Data Handling - -- (UIImage *)eyedropperImage -{ - UIImage *backgroundImage = [self.photoEditor currentResultImage]; - - CGSize fittedSize = TGFitSize(_painting.size, TGPhotoEditorResultImageMaxSize); - UIImage *paintingImage = _painting.isEmpty ? nil : [_painting imageWithSize:fittedSize andData:NULL]; - NSMutableArray *entities = [[NSMutableArray alloc] init]; - - UIImage *entitiesImage = nil; - if (paintingImage == nil && _entitiesContainerView.entitiesCount < 1) - { - return backgroundImage; - } - else if (_entitiesContainerView.entitiesCount > 0) - { - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - TGPhotoPaintEntity *entity = [view entity]; - if (entity != nil) { - [entities addObject:entity]; - } - } - entitiesImage = [_entitiesContainerView imageInRect:_entitiesContainerView.bounds background:nil still:true]; - } - - if (entitiesImage == nil && paintingImage == nil) { - return backgroundImage; - } else { - UIGraphicsBeginImageContextWithOptions(fittedSize, false, 1.0); - - [backgroundImage drawInRect:CGRectMake(0.0, 0.0, fittedSize.width, fittedSize.height)]; - [paintingImage drawInRect:CGRectMake(0.0, 0.0, fittedSize.width, fittedSize.height)]; - [entitiesImage drawInRect:CGRectMake(0.0, 0.0, fittedSize.width, fittedSize.height)]; - - UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return result; - } -} - -- (TGPaintingData *)_prepareResultData -{ - if (_resultData != nil) - return _resultData; - - NSData *data = nil; - CGSize fittedSize = TGFitSize(_painting.size, TGPhotoEditorResultImageMaxSize); - UIImage *image = _painting.isEmpty ? nil : [_painting imageWithSize:fittedSize andData:&data]; - NSMutableArray *entities = [[NSMutableArray alloc] init]; - - bool hasAnimatedEntities = false; - UIImage *stillImage = nil; - if (image == nil && _entitiesContainerView.entitiesCount < 1) - { - _resultData = nil; - return _resultData; - } - else if (_entitiesContainerView.entitiesCount > 0) - { - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - TGPhotoPaintEntity *entity = [view entity]; - if (entity != nil) { - if (entity.animated) { - hasAnimatedEntities = true; - } - [entities addObject:entity]; - } - } - - if (hasAnimatedEntities) { - for (TGPhotoPaintEntity *entity in entities) { - if ([entity isKindOfClass:[TGPhotoPaintTextEntity class]]) { - TGPhotoPaintTextEntity *textEntity = (TGPhotoPaintTextEntity *)entity; - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - if (view.entityUUID == textEntity.uuid) { - textEntity.renderImage = [(TGPhotoTextEntityView *)view image]; - break; - } - } - } - } - } - - if (!hasAnimatedEntities) { - image = [_entitiesContainerView imageInRect:_entitiesContainerView.bounds background:image still:false]; - } else { - stillImage = [_entitiesContainerView imageInRect:_entitiesContainerView.bounds background:image still:true]; - } - } - - _resultData = [TGPaintingData dataWithPaintingData:data image:image stillImage:stillImage entities:entities undoManager:_undoManager]; - return _resultData; -} - -- (UIImage *)image -{ - TGPaintingData *paintingData = [self _prepareResultData]; - return paintingData.image; -} - -- (TGPaintingData *)paintingData -{ - return [self _prepareResultData]; -} - -- (void)enableEyedropper { - if (!_eyedropperView.isHidden) - return; - - [self selectEntityView:nil]; - - self.controlVideoPlayback(false); - [_entitiesContainerView updateVisibility:false]; - - UIImage *image = [self eyedropperImage]; - CGImageRef cgImage = image.CGImage; - CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(cgImage)); - - _eyedropperBackgroundData = (__bridge NSData *)pixelData; - _eyedropperBackgroundSize = image.size; - _eyedropperBackgroundBytesPerRow = CGImageGetBytesPerRow(cgImage); - _eyedropperBackgroundInfo = CGImageGetBitmapInfo(cgImage); - - [_eyedropperView update]; - [_eyedropperView present]; -} - -- (void)commitEyedropper:(bool)immediate { - self.controlVideoPlayback(true); - [_entitiesContainerView updateVisibility:true]; - - _eyedropperBackgroundData = nil; - _eyedropperBackgroundSize = CGSizeZero; - _eyedropperBackgroundBytesPerRow = 0; - _eyedropperBackgroundInfo = 0; - - double timeout = immediate ? 0.0 : 0.2; - TGDispatchAfter(timeout, dispatch_get_main_queue(), ^{ - [_eyedropperView dismiss]; - }); -} - -- (UIColor *)colorFromData:(NSData *)data width:(NSInteger)width height:(NSInteger)height x:(NSInteger)x y:(NSInteger)y bpr:(NSInteger)bpr { - uint8_t *pixel = (uint8_t *)data.bytes + bpr * y + x * 4; - if (_eyedropperBackgroundInfo & kCGBitmapByteOrder32Little) { - return [UIColor colorWithRed:pixel[2] / 255.0 green:pixel[1] / 255.0 blue:pixel[0] / 255.0 alpha:1.0]; - } else { - return [UIColor colorWithRed:pixel[0] / 255.0 green:pixel[1] / 255.0 blue:pixel[2] / 255.0 alpha:1.0]; - } -} - -- (UIColor *)colorAtPoint:(CGPoint)point -{ - CGPoint convertedPoint = CGPointMake(point.x / _eyedropperView.bounds.size.width * _eyedropperBackgroundSize.width, point.y / _eyedropperView.bounds.size.height * _eyedropperBackgroundSize.height); - UIColor *backgroundColor = [self colorFromData:_eyedropperBackgroundData width:_eyedropperBackgroundSize.width height:_eyedropperBackgroundSize.height x:convertedPoint.x y:convertedPoint.y bpr:_eyedropperBackgroundBytesPerRow]; - return backgroundColor; -} - - -#pragma mark - Entities - -- (void)selectEntityView:(TGPhotoPaintEntityView *)view -{ - if (_editedTextView != nil) - return; - - if (_currentEntityView != nil) - { - if (_currentEntityView == view) - { - [self showMenuForEntityView]; - return; - } - - [self _clearCurrentSelection]; - } - - _currentEntityView = view; - [self updateSettingsButton]; - - _scrollView.pinchGestureRecognizer.enabled = _currentEntityView == nil; - - if (view != nil) - { - [_currentEntityView.superview bringSubviewToFront:_currentEntityView]; - } - else - { - [self hideMenu]; - return; - } - - if ([view isKindOfClass:[TGPhotoTextEntityView class]]) - { - TGPaintSwatch *textSwatch = ((TGPhotoPaintTextEntity *)view.entity).swatch; - [self setCurrentSwatch:[TGPaintSwatch swatchWithColor:textSwatch.color colorLocation:textSwatch.colorLocation brushWeight:_portraitSettingsView.swatch.brushWeight] sender:nil]; - } - - _entitySelectionView = [view createSelectionView]; - view.selectionView = _entitySelectionView; - [_selectionContainerView addSubview:_entitySelectionView]; - - __weak TGPhotoPaintController *weakSelf = self; - _entitySelectionView.entityResized = ^(CGFloat scale) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf->_entitySelectionView.entityView scale:scale absolute:true]; - }; - _entitySelectionView.entityRotated = ^(CGFloat angle) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf->_entitySelectionView.entityView rotate:angle absolute:true]; - }; - - [_entitySelectionView update]; -} - -- (void)deleteEntityView:(TGPhotoPaintEntityView *)view -{ - [_undoManager unregisterUndoWithUUID:view.entityUUID]; - - [view removeFromSuperview]; - - [self _clearCurrentSelection]; - - [self updateActionsView]; - [self updateSettingsButton]; -} - -- (void)duplicateEntityView:(TGPhotoPaintEntityView *)view -{ - TGPhotoPaintEntity *entity = [view.entity duplicate]; - entity.position = [self startPositionRelativeToEntity:entity]; - - TGPhotoPaintEntityView *entityView = nil; - if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) - { - TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity]; - [self _commonEntityViewSetup:stickerView]; - entityView = stickerView; - } - else - { - TGPhotoTextEntityView *textView = (TGPhotoTextEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity]; - [self _commonEntityViewSetup:textView]; - entityView = textView; - } - - [self selectEntityView:entityView]; - [self _registerEntityRemovalUndo:entity]; - [self updateActionsView]; -} - -- (void)editEntityView:(TGPhotoPaintEntityView *)view -{ - if ([view isKindOfClass:[TGPhotoTextEntityView class]]) - [(TGPhotoTextEntityView *)view beginEditing]; -} - -#pragma mark Menu - -- (void)showMenuForEntityView -{ - if (_menuContainerView != nil) - { - TGMenuContainerView *container = _menuContainerView; - bool isShowingMenu = container.isShowingMenu; - _menuContainerView = nil; - - [container removeFromSuperview]; - - if (!isShowingMenu && container.menuView.userInfo[@"entity"] == _currentEntityView) - { - if ([_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [self editEntityView:_currentEntityView]; - - return; - } - } - - UIView *parentView = self.view; - _menuContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, parentView.frame.size.width, parentView.frame.size.height)]; - [parentView addSubview:_menuContainerView]; - - NSArray *actions = nil; - - if ([_currentEntityView isKindOfClass:[TGPhotoStickerEntityView class]]) - { - actions = @ - [ - @{ @"title": TGLocalized(@"Paint.Delete"), @"action": @"delete" }, - @{ @"title": TGLocalized(@"Paint.Duplicate"), @"action": @"duplicate" }, - ]; - } - else - { - actions = @ - [ - @{ @"title": TGLocalized(@"Paint.Delete"), @"action": @"delete" }, - @{ @"title": TGLocalized(@"Paint.Edit"), @"action": @"edit" }, - @{ @"title": TGLocalized(@"Paint.Duplicate"), @"action": @"duplicate" }, - ]; - } - - [_menuContainerView.menuView setUserInfo:@{ @"entity": _currentEntityView }]; - [_menuContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle]; - [_menuContainerView.menuView sizeToFit]; - - CGRect sourceRect = CGRectOffset([_currentEntityView convertRect:_currentEntityView.bounds toView:_menuContainerView], 0, -15.0f); - [_menuContainerView showMenuFromRect:sourceRect animated:false]; -} - -- (void)hideMenu -{ - [_menuContainerView hideMenu]; -} - -- (void)actionStageActionRequested:(NSString *)action options:(id)options -{ - if ([action isEqualToString:@"menuAction"]) - { - NSString *menuAction = options[@"action"]; - TGPhotoPaintEntityView *entity = options[@"userInfo"][@"entity"]; - - if ([menuAction isEqualToString:@"delete"]) - { - [self deleteEntityView:entity]; - } - else if ([menuAction isEqualToString:@"duplicate"]) - { - [self duplicateEntityView:entity]; - } - else if ([menuAction isEqualToString:@"edit"]) - { - [self editEntityView:entity]; - } - } - else if ([action isEqualToString:@"menuWillHide"]) - { - } -} - -#pragma mark View - -- (CGPoint)centerPointFittedCropRect -{ - return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesContainerView]; -} - -- (CGFloat)startRotation -{ - return TGCounterRotationForOrientation(_photoEditor.cropOrientation) - _photoEditor.cropRotation; -} - -- (CGPoint)startPositionRelativeToEntity:(TGPhotoPaintEntity *)entity -{ - const CGPoint offset = CGPointMake(200.0f, 200.0f); - - if (entity != nil) - { - return TGPaintAddPoints(entity.position, offset); - } - else - { - const CGFloat minimalDistance = 100.0f; - CGPoint position = [self centerPointFittedCropRect]; - - while (true) - { - bool occupied = false; - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoPaintEntityView class]]) - continue; - - CGPoint location = view.center; - CGFloat distance = sqrt(pow(location.x - position.x, 2) + pow(location.y - position.y, 2)); - if (distance < minimalDistance) - occupied = true; - } - - if (!occupied) - break; - else - position = TGPaintAddPoints(position, offset); - } - - return position; - } -} - -- (void)_commonEntityViewSetup:(TGPhotoPaintEntityView *)entityView -{ - [self hideMenu]; - - __weak TGPhotoPaintController *weakSelf = self; - entityView.shouldTouchEntity = ^bool (__unused TGPhotoPaintEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - return ![strongSelf->_canvasView isTracking] && ![strongSelf->_entitiesContainerView isTrackingAnyEntityView]; - }; - entityView.entityBeganDragging = ^(TGPhotoPaintEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil && sender != strongSelf->_entitySelectionView.entityView) - [strongSelf selectEntityView:sender]; - }; - entityView.entityChanged = ^(TGPhotoPaintEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if (sender == strongSelf->_entitySelectionView.entityView) - [strongSelf->_entitySelectionView update]; - - [strongSelf updateActionsView]; - }; - - if ([entityView isKindOfClass:[TGPhotoTextEntityView class]]) { - TGPhotoTextEntityView *textView = (TGPhotoTextEntityView *)entityView; - - __weak TGPhotoPaintController *weakSelf = self; - textView.beganEditing = ^(TGPhotoTextEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf bringTextEntityViewFront:sender]; - }; - - textView.finishedEditing = ^(__unused TGPhotoTextEntityView *sender) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - [strongSelf sendTextEntityViewBack]; - }; - } -} - -- (void)_registerEntityRemovalUndo:(TGPhotoPaintEntity *)entity -{ - [_undoManager registerUndoWithUUID:entity.uuid block:^(__unused TGPainting *painting, TGPhotoEntitiesContainerView *entitiesContainer, NSInteger uuid) - { - [entitiesContainer removeViewWithUUID:uuid]; - }]; -} - -#pragma mark Stickers - -- (void)presentStickersView -{ - if (_stickersScreen != nil) { - [_stickersScreen restore]; - return; - } - - __weak TGPhotoPaintController *weakSelf = self; - _stickersScreen = _stickersContext.presentStickersController(^(id document, bool animated, UIView *view, CGRect rect) { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) { - [strongSelf createNewStickerWithDocument:document animated:animated transitionPoint:CGPointZero snapshotView:nil]; - } - }); - _stickersScreen.screenDidAppear = ^{ - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) { - strongSelf.controlVideoPlayback(false); - [strongSelf->_entitiesContainerView updateVisibility:false]; - } - }; - _stickersScreen.screenWillDisappear = ^{ - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) { - strongSelf.controlVideoPlayback(true); - [strongSelf->_entitiesContainerView updateVisibility:true]; - } - }; -} - -- (void)createNewStickerWithDocument:(id)document animated:(bool)animated transitionPoint:(CGPoint)transitionPoint snapshotView:(UIView *)snapshotView -{ - TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:document baseSize:[self _stickerBaseSizeForCurrentPainting] animated:animated]; - [self _setStickerEntityPosition:entity]; - - - TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity]; - - bool hasStickers = false; - TGPhotoStickerEntityView *existingStickerView; - for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) { - if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) { - hasStickers = true; - - if (((TGPhotoStickerEntityView *)view).documentId == stickerView.documentId) { - existingStickerView = (TGPhotoStickerEntityView *)view; - } - break; - } - } - - [_entitiesContainerView addSubview:stickerView]; - [self _commonEntityViewSetup:stickerView]; - - __weak TGPhotoPaintController *weakSelf = self; - stickerView.started = ^(double duration) { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) { - TGPhotoEditorController *editorController = (TGPhotoEditorController *)strongSelf.parentViewController; - if (![editorController isKindOfClass:[TGPhotoEditorController class]]) - return; - - if (!hasStickers) { - [editorController setMinimalVideoDuration:duration]; - } - } - }; - - NSTimeInterval currentTime = NAN; - NSTimeInterval stickerStartTime = _stickerStartTime; - TGPhotoEditorController *editorController = (TGPhotoEditorController *)self.parentViewController; - if ([editorController isKindOfClass:[TGPhotoEditorController class]]) { - currentTime = editorController.currentTime; - } - - if (!isnan(currentTime)) { - [stickerView seekTo:currentTime]; - [stickerView play]; - } else { - NSTimeInterval currentTime = CACurrentMediaTime(); - if (!isnan(stickerStartTime)) { - if (existingStickerView != nil) { - [stickerView copyStickerView:existingStickerView]; - } else { - NSTimeInterval position = currentTime - stickerStartTime; - [stickerView seekTo:position]; - [stickerView play]; - } - } else { - _stickerStartTime = currentTime; - [stickerView play]; - } - } - - [self selectEntityView:stickerView]; - _entitySelectionView.alpha = 0.0f; - - [_entitySelectionView fadeIn]; - - [self _registerEntityRemovalUndo:entity]; - [self updateActionsView]; -} - -- (void)mirrorSelectedStickerEntity -{ - if ([_currentEntityView isKindOfClass:[TGPhotoStickerEntityView class]]) - [((TGPhotoStickerEntityView *)_currentEntityView) mirror]; -} - -#pragma mark Text - -- (void)createNewTextLabel -{ - TGPaintSwatch *currentSwatch = _portraitSettingsView.swatch; - TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0xffffff) colorLocation:1.0f brushWeight:currentSwatch.brushWeight]; - TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0x000000) colorLocation:0.85f brushWeight:currentSwatch.brushWeight]; - [self setCurrentSwatch:_selectedTextStyle == TGPhotoPaintTextEntityStyleOutlined ? blackSwatch : whiteSwatch sender:nil]; - - CGFloat maxWidth = [self fittedContentSize].width - 26.0f; - TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:@"" font:_selectedTextFont swatch:_portraitSettingsView.swatch baseFontSize:[self _textBaseFontSizeForCurrentPainting] maxWidth:maxWidth style:_selectedTextStyle]; - entity.position = [self startPositionRelativeToEntity:nil]; - entity.angle = [self startRotation]; - - TGPhotoTextEntityView *textView = (TGPhotoTextEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity]; - [_entitiesContainerView addSubview:textView]; - [self _commonEntityViewSetup:textView]; - - [self selectEntityView:textView]; - - [self _registerEntityRemovalUndo:entity]; - [self updateActionsView]; - - [textView beginEditing]; -} - -- (void)bringTextEntityViewFront:(TGPhotoTextEntityView *)entityView -{ - _editedTextView = entityView; - entityView.inhibitGestures = true; - - [_dimView.superview insertSubview:_dimView belowSubview:entityView]; - - _textEditingDismissButton = [[UIButton alloc] initWithFrame:_dimView.bounds]; - _dimView.userInteractionEnabled = true; - [_textEditingDismissButton addTarget:self action:@selector(_dismissButtonTapped) forControlEvents:UIControlEventTouchUpInside]; - [_dimView addSubview:_textEditingDismissButton]; - - _editedTextCenter = entityView.center; - _editedTextTransform = entityView.transform; - - _entitySelectionView.alpha = 0.0f; - - void (^changeBlock)(void) = ^ - { - entityView.center = [self centerPointFittedCropRect]; - entityView.transform = CGAffineTransformMakeRotation([self startRotation]); - - _dimView.alpha = 1.0f; - }; - - _contentView.userInteractionEnabled = true; - _contentWrapperView.userInteractionEnabled = true; - - if (iosMajorVersion() >= 7) - { - [UIView animateWithDuration:0.4 delay:0.0 usingSpringWithDamping:0.8f initialSpringVelocity:0.0f options:kNilOptions animations:changeBlock completion:nil]; - } - else - { - [UIView animateWithDuration:0.35 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:changeBlock completion:nil]; - } - - [self setInterfaceHidden:true animated:true]; -} - -- (void)_dismissButtonTapped -{ - TGPhotoTextEntityView *entityView = _editedTextView; - [entityView endEditing]; -} - -- (void)sendTextEntityViewBack -{ - _contentView.userInteractionEnabled = false; - _contentWrapperView.userInteractionEnabled = false; - - _dimView.userInteractionEnabled = false; - [_textEditingDismissButton removeFromSuperview]; - _textEditingDismissButton = nil; - - TGPhotoTextEntityView *entityView = _editedTextView; - _editedTextView = nil; - - void (^changeBlock)(void) = ^ - { - entityView.center = _editedTextCenter; - entityView.transform = _editedTextTransform; - _dimView.alpha = 0.0f; - }; - - void (^completionBlock)(BOOL) = ^(__unused BOOL finished) - { - [_dimView.superview bringSubviewToFront:_dimView]; - entityView.inhibitGestures = false; - - if (entityView.isEmpty) - { - [self deleteEntityView:entityView]; - } - else - { - [_entitySelectionView update]; - [_entitySelectionView fadeIn]; - } - }; - - if (iosMajorVersion() >= 7) - { - [UIView animateWithDuration:0.4 delay:0.0 usingSpringWithDamping:0.8f initialSpringVelocity:0.0f options:kNilOptions animations:changeBlock completion:completionBlock]; - } - else - { - [UIView animateWithDuration:0.35 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:changeBlock completion:completionBlock]; - } - - [self setInterfaceHidden:false animated:true]; - - TGMenuContainerView *container = _menuContainerView; - _menuContainerView = nil; - [container removeFromSuperview]; -} - -- (void)containerPressed -{ - if (_currentEntityView == nil) - return; - - if ([_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - { - TGPhotoTextEntityView *textEntityView = (TGPhotoTextEntityView *)_currentEntityView; - if ([textEntityView isEditing]) - { - [textEntityView endEditing]; - return; - } - } - [self selectEntityView:nil]; -} - -#pragma mark - Relative Size Calculation - -- (CGSize)_stickerBaseSizeForCurrentPainting -{ - CGSize fittedSize = [self fittedContentSize]; - CGFloat maxSide = MAX(fittedSize.width, fittedSize.height); - CGFloat side = ceil(maxSide * 0.3125f); - return CGSizeMake(side, side); -} - -- (CGFloat)_textBaseFontSizeForCurrentPainting -{ - CGSize fittedSize = [self fittedContentSize]; - CGFloat maxSide = MAX(fittedSize.width, fittedSize.height); - return ceil(maxSide * 0.08f); -} - -- (CGFloat)_brushBaseWeightForCurrentPainting -{ - return 15.0f / TGPhotoPaintingMaxSize.width * _painting.size.width; -} - -- (CGFloat)_brushWeightRangeForCurrentPainting -{ - return 125.0f / TGPhotoPaintingMaxSize.width * _painting.size.width; -} - -- (CGFloat)_brushWeightForSize:(CGFloat)size -{ - CGFloat scale = MAX(0.001, _scrollView.zoomScale); - return ([self _brushBaseWeightForCurrentPainting] + [self _brushWeightRangeForCurrentPainting] * size) / scale; -} - -+ (CGSize)maximumPaintingSize -{ - static dispatch_once_t onceToken; - static CGSize size; - dispatch_once(&onceToken, ^ - { - CGSize screenSize = TGScreenSize(); - if ((NSInteger)screenSize.height == 480) - size = TGPhotoPaintingLightMaxSize; - else - size = TGPhotoPaintingMaxSize; - }); - return size; -} - -#pragma mark - Settings - -- (void)setCurrentSwatch:(TGPaintSwatch *)swatch sender:(id)sender -{ - [_canvasView setBrushColor:swatch.color]; - [_canvasView setBrushWeight:[self _brushWeightForSize:swatch.brushWeight]]; - if ([_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [(TGPhotoTextEntityView *)_currentEntityView setSwatch:swatch]; - - if (sender != _landscapeSettingsView) - [_landscapeSettingsView setSwatch:swatch]; - - if (sender != _portraitSettingsView) - [_portraitSettingsView setSwatch:swatch]; -} - -- (void)updateSettingsButton -{ - if ([_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) { - TGPhotoPaintSettingsViewIcon icon; - switch (((TGPhotoTextEntityView *)_currentEntityView).entity.style) { - case TGPhotoPaintTextEntityStyleRegular: - icon = TGPhotoPaintSettingsViewIconTextRegular; - break; - case TGPhotoPaintTextEntityStyleOutlined: - icon = TGPhotoPaintSettingsViewIconTextOutlined; - break; - case TGPhotoPaintTextEntityStyleFramed: - icon = TGPhotoPaintSettingsViewIconTextFramed; - break; - } - [self setSettingsButtonIcon:icon]; - } - else if ([_currentEntityView isKindOfClass:[TGPhotoStickerEntityView class]]) { - [self setSettingsButtonIcon:TGPhotoPaintSettingsViewIconMirror]; - } - else { - TGPhotoPaintSettingsViewIcon icon = TGPhotoPaintSettingsViewIconBrushPen; - if ([_canvasView.state.brush isKindOfClass:[TGPaintEllipticalBrush class]]) { - icon = TGPhotoPaintSettingsViewIconBrushMarker; - } else if ([_canvasView.state.brush isKindOfClass:[TGPaintNeonBrush class]]) { - icon = TGPhotoPaintSettingsViewIconBrushNeon; - } else if ([_canvasView.state.brush isKindOfClass:[TGPaintArrowBrush class]]) { - icon = TGPhotoPaintSettingsViewIconBrushArrow; - } - [self setSettingsButtonIcon:icon]; - } - [self _updateTabs]; -} - -- (void)setSettingsButtonIcon:(TGPhotoPaintSettingsViewIcon)icon -{ - [_portraitSettingsView setIcon:icon animated:true]; - [_landscapeSettingsView setIcon:icon animated:true]; -} - -- (void)settingsWrapperPressed -{ - [_settingsView dismissWithCompletion:^ - { - [_settingsView removeFromSuperview]; - _settingsView = nil; - - [_settingsViewWrapper removeFromSuperview]; - }]; -} - -- (UIView *)settingsViewWrapper -{ - if (_settingsViewWrapper == nil) - { - _settingsViewWrapper = [[TGPhotoPaintSettingsWrapperView alloc] initWithFrame:self.parentViewController.view.bounds]; - _settingsViewWrapper.exclusiveTouch = true; - - __weak TGPhotoPaintController *weakSelf = self; - _settingsViewWrapper.pressed = ^(__unused CGPoint location) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf != nil) - [strongSelf settingsWrapperPressed]; - }; - _settingsViewWrapper.suppressTouchAtPoint = ^bool(CGPoint location) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return false; - - UIView *view = [strongSelf.view hitTest:[strongSelf.view convertPoint:location fromView:nil] withEvent:nil]; - if ([view isKindOfClass:[TGModernButton class]]) - return true; - - if ([view isKindOfClass:[TGPaintCanvas class]]) - return true; - - if (view == strongSelf->_portraitToolsWrapperView || view == strongSelf->_landscapeToolsWrapperView) - return true; - - return false; - }; - } - - [self.parentViewController.view addSubview:_settingsViewWrapper]; - - return _settingsViewWrapper; -} - -- (TGPaintBrushPreview *)brushPreview -{ - if ([_brushes.firstObject previewImage] != nil) - return nil; - - if (_brushPreview == nil) - _brushPreview = [[TGPaintBrushPreview alloc] init]; - - return _brushPreview; -} - -- (void)presentBrushSettingsView -{ - TGPhotoBrushSettingsView *view = [[TGPhotoBrushSettingsView alloc] initWithBrushes:_brushes preview:[self brushPreview]]; - [view setBrush:_painting.brush]; - - __weak TGPhotoPaintController *weakSelf = self; - view.brushChanged = ^(TGPaintBrush *brush) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if (strongSelf->_canvasView.state.eraser && (brush.lightSaber || brush.arrow)) - brush = strongSelf->_brushes.firstObject; - - [strongSelf->_canvasView setBrush:brush]; - - [strongSelf settingsWrapperPressed]; - [strongSelf updateSettingsButton]; - }; - _settingsView = view; - [view sizeToFit]; - - UIView *wrapper = [self settingsViewWrapper]; - wrapper.userInteractionEnabled = true; - [wrapper addSubview:view]; - - [self viewWillLayoutSubviews]; - - [view present]; -} - -- (void)presentTextSettingsView -{ - TGPhotoTextSettingsView *view = [[TGPhotoTextSettingsView alloc] initWithFonts:[TGPhotoPaintFont availableFonts] selectedFont:_selectedTextFont selectedStyle:_selectedTextStyle]; - - __weak TGPhotoPaintController *weakSelf = self; - view.fontChanged = ^(TGPhotoPaintFont *font) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - strongSelf->_selectedTextFont = font; - - TGPhotoTextEntityView *textView = (TGPhotoTextEntityView *)strongSelf->_currentEntityView; - [textView setFont:font]; - - [strongSelf settingsWrapperPressed]; - [strongSelf updateSettingsButton]; - }; - view.styleChanged = ^(TGPhotoPaintTextEntityStyle style) - { - __strong TGPhotoPaintController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - strongSelf->_selectedTextStyle = style; - - if (style == TGPhotoPaintTextEntityStyleOutlined && [strongSelf->_portraitSettingsView.swatch.color isEqual:UIColorRGB(0xffffff)]) - { - TGPaintSwatch *currentSwatch = strongSelf->_portraitSettingsView.swatch; - TGPaintSwatch *blackSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0x000000) colorLocation:0.85f brushWeight:currentSwatch.brushWeight]; - [strongSelf setCurrentSwatch:blackSwatch sender:nil]; - } - else if (style != TGPhotoPaintTextEntityStyleOutlined && [strongSelf->_portraitSettingsView.swatch.color isEqual:UIColorRGB(0x000000)]) - { - TGPaintSwatch *currentSwatch = strongSelf->_portraitSettingsView.swatch; - TGPaintSwatch *whiteSwatch = [TGPaintSwatch swatchWithColor:UIColorRGB(0xffffff) colorLocation:1.0f brushWeight:currentSwatch.brushWeight]; - [strongSelf setCurrentSwatch:whiteSwatch sender:nil]; - } - - TGPhotoTextEntityView *textView = (TGPhotoTextEntityView *)strongSelf->_currentEntityView; - [textView setStyle:style]; - - [strongSelf settingsWrapperPressed]; - [strongSelf updateSettingsButton]; - }; - - _settingsView = view; - [view sizeToFit]; - - UIView *wrapper = [self settingsViewWrapper]; - wrapper.userInteractionEnabled = true; - [wrapper addSubview:view]; - - [self viewWillLayoutSubviews]; - - [view present]; -} - -- (void)toggleEraserMode -{ - _canvasView.state.eraser = !_canvasView.state.isEraser; - - if (_canvasView.state.eraser) - { - if (_canvasView.state.brush.lightSaber || _canvasView.state.brush.arrow) - [_canvasView setBrush:_brushes.firstObject]; - } - - [_portraitSettingsView setHighlighted:_canvasView.state.isEraser]; - [_landscapeSettingsView setHighlighted:_canvasView.state.isEraser]; - - [self updateSettingsButton]; - [self _updateTabs]; -} - -#pragma mark - Scroll View - -- (CGSize)fittedContentSize -{ - return [TGPhotoPaintController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize]; -} - -+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize { - CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); - CGFloat scale = fittedOriginalSize.width / originalSize.width; - - CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); - if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) - size = CGSizeMake(size.height, size.width); - - return CGSizeMake(floor(size.width), floor(size.height)); -} - -- (CGRect)fittedCropRect:(bool)originalSize -{ - return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize]; -} - -+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize { - CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]); - CGFloat scale = fittedOriginalSize.width / originalSize.width; - - CGSize size = fittedOriginalSize; - if (!keepOriginalSize) - size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); - - return CGRectMake(-cropRect.origin.x * scale, -cropRect.origin.y * scale, size.width, size.height); -} - -- (CGPoint)fittedCropCenterScale:(CGFloat)scale -{ - return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect centerScale:scale]; -} - -+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale -{ - CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale); - CGRect rect = CGRectMake(cropRect.origin.x * scale, cropRect.origin.y * scale, size.width, size.height); - - return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); -} - -- (void)resetScrollView -{ - CGSize fittedContentSize = [self fittedContentSize]; - CGRect fittedCropRect = [self fittedCropRect:false]; - _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height); - - CGFloat scale = _contentView.bounds.size.width / fittedCropRect.size.width; - _contentWrapperView.transform = CGAffineTransformMakeScale(scale, scale); - _contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height); - - CGSize contentSize = [self contentSize]; - _scrollView.minimumZoomScale = 1.0f; - _scrollView.maximumZoomScale = 1.0f; - _scrollView.normalZoomScale = 1.0f; - _scrollView.zoomScale = 1.0f; - _scrollView.contentSize = contentSize; - [self contentView].frame = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); - - [self adjustZoom]; - _scrollView.zoomScale = _scrollView.normalZoomScale; -} - -- (void)scrollViewWillBeginZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view -{ -} - -- (void)scrollViewDidZoom:(UIScrollView *)__unused scrollView -{ - [self adjustZoom]; -} - -- (void)scrollViewDidEndZooming:(UIScrollView *)__unused scrollView withView:(UIView *)__unused view atScale:(CGFloat)__unused scale -{ - [self adjustZoom]; - - TGPaintSwatch *currentSwatch = _portraitSettingsView.swatch; - [_canvasView setBrushWeight:[self _brushWeightForSize:currentSwatch.brushWeight]]; - - if (_scrollView.zoomScale < _scrollView.normalZoomScale - FLT_EPSILON) - { - [TGHacks setAnimationDurationFactor:0.5f]; - [_scrollView setZoomScale:_scrollView.normalZoomScale animated:true]; - [TGHacks setAnimationDurationFactor:1.0f]; - } -} - -- (UIView *)contentView -{ - return _scrollContentView; -} - -- (CGSize)contentSize -{ - return _scrollView.frame.size; -} - -- (UIView *)viewForZoomingInScrollView:(UIScrollView *)__unused scrollView -{ - return [self contentView]; -} - -- (void)adjustZoom -{ - CGSize contentSize = [self contentSize]; - CGSize boundsSize = _scrollView.frame.size; - if (contentSize.width < FLT_EPSILON || contentSize.height < FLT_EPSILON || boundsSize.width < FLT_EPSILON || boundsSize.height < FLT_EPSILON) - return; - - CGFloat scaleWidth = boundsSize.width / contentSize.width; - CGFloat scaleHeight = boundsSize.height / contentSize.height; - CGFloat minScale = MIN(scaleWidth, scaleHeight); - CGFloat maxScale = MAX(scaleWidth, scaleHeight); - maxScale = MAX(maxScale, minScale * 3.0f); - - if (ABS(maxScale - minScale) < 0.01f) - maxScale = minScale; - - _scrollView.contentInset = UIEdgeInsetsZero; - - if (_scrollView.minimumZoomScale != 0.05f) - _scrollView.minimumZoomScale = 0.05f; - if (_scrollView.normalZoomScale != minScale) - _scrollView.normalZoomScale = minScale; - if (_scrollView.maximumZoomScale != maxScale) - _scrollView.maximumZoomScale = maxScale; - - CGRect contentFrame = [self contentView].frame; - - if (boundsSize.width > contentFrame.size.width) - contentFrame.origin.x = (boundsSize.width - contentFrame.size.width) / 2.0f; - else - contentFrame.origin.x = 0; - - if (boundsSize.height > contentFrame.size.height) - contentFrame.origin.y = (boundsSize.height - contentFrame.size.height) / 2.0f; - else - contentFrame.origin.y = 0; - - [self contentView].frame = contentFrame; - - _scrollView.scrollEnabled = ABS(_scrollView.zoomScale - _scrollView.normalZoomScale) > FLT_EPSILON; -} - -#pragma mark - Gestures - -- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer -{ - [_entitiesContainerView handlePinch:gestureRecognizer]; -} - -- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer -{ - [_entitiesContainerView handleRotate:gestureRecognizer]; -} - -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)__unused gestureRecognizer -{ - if (gestureRecognizer == _pinchGestureRecognizer && _currentEntityView == nil) { - return false; - } - return !_canvasView.isTracking; -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer -{ - return true; -} - -#pragma mark - Transitions - -- (void)transitionIn -{ - _portraitSettingsView.layer.shouldRasterize = true; - _landscapeSettingsView.layer.shouldRasterize = true; - - [UIView animateWithDuration:0.3f animations:^ - { - _portraitToolsWrapperView.alpha = 1.0f; - _landscapeToolsWrapperView.alpha = 1.0f; - - _portraitActionsView.alpha = 1.0f; - _landscapeActionsView.alpha = 1.0f; - } completion:^(__unused BOOL finished) - { - _portraitSettingsView.layer.shouldRasterize = false; - _landscapeSettingsView.layer.shouldRasterize = false; - }]; - - if (self.presentedForAvatarCreation) { - _canvasView.hidden = true; - } -} - -+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation -{ - CGRect frame = [TGPhotoEditorTabController photoContainerFrameForParentViewFrame:parentViewFrame toolbarLandscapeSize:toolbarLandscapeSize orientation:orientation panelSize:panelSize hasOnScreenNavigation:hasOnScreenNavigation]; - - switch (orientation) - { - case UIInterfaceOrientationLandscapeLeft: - frame.origin.x -= TGPhotoPaintTopPanelSize; - break; - - case UIInterfaceOrientationLandscapeRight: - frame.origin.x += TGPhotoPaintTopPanelSize; - break; - - default: - frame.origin.y += TGPhotoPaintTopPanelSize; - break; - } - - return frame; -} - -- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame -{ - CGSize referenceSize = [self referenceViewSize]; - CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; - - CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size); - CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); - - return toFrame; -} - -- (void)_finishedTransitionInWithView:(UIView *)transitionView -{ - _appeared = true; - - if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) { - - } else { - [transitionView removeFromSuperview]; - } - - [self setupCanvas]; - _entitiesContainerView.hidden = false; - - TGPhotoEditorPreviewView *previewView = _previewView; - [previewView setPaintingHidden:true]; - previewView.hidden = false; - [_containerView insertSubview:previewView belowSubview:_paintingWrapperView]; - [self updateContentViewLayout]; - [previewView performTransitionInIfNeeded]; - - CGRect rect = [self fittedCropRect:true]; - _entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height); - _entitiesContainerView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation); - - CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]); - CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation); - CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); - - CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; - CGPoint offset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); - - CGPoint boundsCenter = TGPaintCenterOfRect(_contentWrapperView.bounds); - _entitiesContainerView.center = TGPaintAddPoints(boundsCenter, offset); - - if (!_skipEntitiesSetup || _entitiesReady) { - [_contentWrapperView addSubview:_entitiesContainerView]; - } - _entitiesReady = true; - [self resetScrollView]; -} - -- (void)prepareForCustomTransitionOut -{ - _previewView.hidden = true; - _canvasView.hidden = true; - _contentView.hidden = true; - [UIView animateWithDuration:0.3f animations:^ - { - _portraitToolsWrapperView.alpha = 0.0f; - _landscapeToolsWrapperView.alpha = 0.0f; - } completion:nil]; -} - -- (void)transitionOutSwitching:(bool)__unused switching completion:(void (^)(void))completion -{ - [_stickersScreen invalidate]; - - TGPhotoEditorPreviewView *previewView = self.previewView; - previewView.interactionEnded = nil; - - _portraitSettingsView.layer.shouldRasterize = true; - _landscapeSettingsView.layer.shouldRasterize = true; - - [UIView animateWithDuration:0.3f animations:^ - { - _portraitToolsWrapperView.alpha = 0.0f; - _landscapeToolsWrapperView.alpha = 0.0f; - - _portraitActionsView.alpha = 0.0f; - _landscapeActionsView.alpha = 0.0f; - } completion:^(__unused BOOL finished) - { - if (completion != nil) - completion(); - }]; -} - -- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation -{ - CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; - - CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size); - return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); -} - -- (void)_animatePreviewViewTransitionOutToFrame:(CGRect)targetFrame saving:(bool)saving parentView:(UIView *)parentView completion:(void (^)(void))completion -{ - _dismissing = true; - - [_entitySelectionView removeFromSuperview]; - _entitySelectionView = nil; - - TGPhotoEditorPreviewView *previewView = self.previewView; - [previewView prepareForTransitionOut]; - - UIInterfaceOrientation orientation = self.effectiveOrientation; - CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; - CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height); - CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y); - previewView.frame = rect; - - UIView *snapshotView = nil; - POPSpringAnimation *snapshotAnimation = nil; - NSMutableArray *animations = [[NSMutableArray alloc] init]; - - if (saving && CGRectIsNull(targetFrame) && parentView != nil) - { - snapshotView = [previewView snapshotViewAfterScreenUpdates:false]; - snapshotView.frame = [_containerView convertRect:previewView.frame toView:parentView]; - - UIView *canvasSnapshotView = [_paintingWrapperView resizableSnapshotViewFromRect:[_paintingWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; - canvasSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - canvasSnapshotView.transform = _contentView.transform; - canvasSnapshotView.frame = snapshotView.bounds; - [snapshotView addSubview:canvasSnapshotView]; - - UIView *entitiesSnapshotView = [_contentWrapperView resizableSnapshotViewFromRect:[_contentWrapperView convertRect:previewView.bounds fromView:previewView] afterScreenUpdates:false withCapInsets:UIEdgeInsetsZero]; - entitiesSnapshotView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - entitiesSnapshotView.transform = _contentView.transform; - entitiesSnapshotView.frame = snapshotView.bounds; - [snapshotView addSubview:entitiesSnapshotView]; - - CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size); - targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); - - [parentView addSubview:snapshotView]; - - snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; - snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame]; - snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; - [animations addObject:snapshotAnimation]; - } - - targetFrame = CGRectOffset(targetFrame, -containerFrame.origin.x, -containerFrame.origin.y); - CGPoint targetCenter = TGPaintCenterOfRect(targetFrame); - - POPSpringAnimation *previewAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame]; - previewAnimation.fromValue = [NSValue valueWithCGRect:previewView.frame]; - previewAnimation.toValue = [NSValue valueWithCGRect:targetFrame]; - [animations addObject:previewAnimation]; - - POPSpringAnimation *previewAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; - previewAlphaAnimation.fromValue = @(previewView.alpha); - previewAlphaAnimation.toValue = @(0.0f); - [animations addObject:previewAnimation]; - - POPSpringAnimation *entitiesAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; - entitiesAnimation.fromValue = [NSValue valueWithCGPoint:_contentView.center]; - entitiesAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; - [animations addObject:entitiesAnimation]; - - CGFloat targetEntitiesScale = targetFrame.size.width / _contentView.frame.size.width; - POPSpringAnimation *entitiesScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; - entitiesScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; - entitiesScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetEntitiesScale, targetEntitiesScale)]; - [animations addObject:entitiesScaleAnimation]; - - POPSpringAnimation *entitiesAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; - entitiesAlphaAnimation.fromValue = @(_canvasView.alpha); - entitiesAlphaAnimation.toValue = @(0.0f); - [animations addObject:entitiesAlphaAnimation]; - - POPSpringAnimation *paintingAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewCenter]; - paintingAnimation.fromValue = [NSValue valueWithCGPoint:_paintingWrapperView.center]; - paintingAnimation.toValue = [NSValue valueWithCGPoint:targetCenter]; - [animations addObject:paintingAnimation]; - - CGFloat targetPaintingScale = targetFrame.size.width / _paintingWrapperView.frame.size.width; - POPSpringAnimation *paintingScaleAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewScaleXY]; - paintingScaleAnimation.fromValue = [NSValue valueWithCGSize:CGSizeMake(1.0f, 1.0f)]; - paintingScaleAnimation.toValue = [NSValue valueWithCGSize:CGSizeMake(targetPaintingScale, targetPaintingScale)]; - [animations addObject:paintingScaleAnimation]; - - POPSpringAnimation *paintingAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha]; - paintingAlphaAnimation.fromValue = @(_paintingWrapperView.alpha); - paintingAlphaAnimation.toValue = @(0.0f); - [animations addObject:paintingAlphaAnimation]; - - [TGPhotoEditorAnimation performBlock:^(__unused bool allFinished) - { - [snapshotView removeFromSuperview]; - - if (completion != nil) - completion(); - } whenCompletedAllAnimations:animations]; - - if (snapshotAnimation != nil) - [snapshotView pop_addAnimation:snapshotAnimation forKey:@"frame"]; - [previewView pop_addAnimation:previewAnimation forKey:@"frame"]; - [previewView pop_addAnimation:previewAlphaAnimation forKey:@"alpha"]; - - [_contentView pop_addAnimation:entitiesAnimation forKey:@"frame"]; - [_contentView pop_addAnimation:entitiesScaleAnimation forKey:@"scale"]; - [_contentView pop_addAnimation:entitiesAlphaAnimation forKey:@"alpha"]; - - [_paintingWrapperView pop_addAnimation:paintingAnimation forKey:@"frame"]; - [_paintingWrapperView pop_addAnimation:paintingScaleAnimation forKey:@"scale"]; - [_paintingWrapperView pop_addAnimation:paintingAlphaAnimation forKey:@"alpha"]; - - if (saving) - { - _contentView.hidden = true; - _paintingWrapperView.hidden = true; - previewView.hidden = true; - } -} - -- (CGRect)transitionOutReferenceFrame -{ - TGPhotoEditorPreviewView *previewView = _previewView; - return [previewView convertRect:previewView.bounds toView:self.view]; -} - -- (UIView *)transitionOutReferenceView -{ - return _previewView; -} - -- (UIView *)snapshotView -{ - TGPhotoEditorPreviewView *previewView = self.previewView; - return [previewView originalSnapshotView]; -} - -- (void)setInterfaceHidden:(bool)hidden animated:(bool)animated -{ - CGFloat targetAlpha = hidden ? 0.0f : 1.0; - void (^changeBlock)(void) = ^ - { - _portraitActionsView.alpha = targetAlpha; - _landscapeActionsView.alpha = targetAlpha; - _portraitSettingsView.alpha = targetAlpha; - _landscapeSettingsView.alpha = targetAlpha; - }; - - if (animated) - [UIView animateWithDuration:0.25 animations:changeBlock]; - else - changeBlock(); - - TGPhotoEditorController *editorController = (TGPhotoEditorController *)self.parentViewController; - if (![editorController isKindOfClass:[TGPhotoEditorController class]]) - return; - - [editorController setToolbarHidden:hidden animated:animated]; -} - -- (void)setDimHidden:(bool)hidden animated:(bool)animated -{ - if (!hidden) - { - [_entitySelectionView fadeOut]; - - if ([_currentEntityView isKindOfClass:[TGPhotoTextEntityView class]]) - [_dimView.superview insertSubview:_dimView belowSubview:_currentEntityView]; - else - [_dimView.superview bringSubviewToFront:_dimView]; - - [_doneButton.superview bringSubviewToFront:_doneButton]; - } - else - { - [_entitySelectionView fadeIn]; - - [_dimView.superview bringSubviewToFront:_dimView]; - - [_doneButton.superview bringSubviewToFront:_doneButton]; - } - - void (^changeBlock)(void) = ^ - { - _dimView.alpha = hidden ? 0.0f : 1.0f; - _doneButton.alpha = hidden ? 0.0f : 1.0f; - }; - - if (animated) - [UIView animateWithDuration:0.25 animations:changeBlock]; - else - changeBlock(); -} - -- (id)currentResultRepresentation -{ - return TGPaintCombineCroppedImages(self.photoEditor.currentResultImage, [self image], true, _photoEditor.originalSize, _photoEditor.cropRect, _photoEditor.cropOrientation, _photoEditor.cropRotation, false); -} - -#pragma mark - Layout - -- (void)viewWillLayoutSubviews -{ - [super viewWillLayoutSubviews]; - - [self updateLayout:[[LegacyComponentsGlobals provider] applicationStatusBarOrientation]]; - [_entitySelectionView update]; -} - -- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration -{ - [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; - - if (_menuContainerView != nil) - { - [_menuContainerView removeFromSuperview]; - _menuContainerView = nil; - } - - [self updateLayout:toInterfaceOrientation]; -} - -- (void)updateContentViewLayout -{ - CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(_photoEditor.cropOrientation)); - _contentView.transform = rotationTransform; - _contentView.frame = self.previewView.frame; - [self resetScrollView]; -} - -- (void)updateLayout:(UIInterfaceOrientation)orientation -{ - if ([self inFormSheet] || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) - { - _landscapeToolsWrapperView.hidden = true; - orientation = UIInterfaceOrientationPortrait; - } - - CGSize referenceSize = [self referenceViewSize]; - CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; - - bool sizeUpdated = false; - if (!CGSizeEqualToSize(referenceSize, _previousSize)) { - sizeUpdated = true; - _previousSize = referenceSize; - } - - CGFloat panelToolbarPortraitSize = TGPhotoPaintBottomPanelSize + TGPhotoEditorToolbarSize; - CGFloat panelToolbarLandscapeSize = TGPhotoPaintBottomPanelSize + self.toolbarLandscapeSize; - - UIEdgeInsets safeAreaInset = [TGViewController safeAreaInsetForOrientation:orientation hasOnScreenNavigation:self.hasOnScreenNavigation]; - UIEdgeInsets screenEdges = UIEdgeInsetsMake((screenSide - referenceSize.height) / 2, (screenSide - referenceSize.width) / 2, (screenSide + referenceSize.height) / 2, (screenSide + referenceSize.width) / 2); - screenEdges.top += safeAreaInset.top; - screenEdges.left += safeAreaInset.left; - screenEdges.bottom -= safeAreaInset.bottom; - screenEdges.right -= safeAreaInset.right; - - CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; - - _settingsViewWrapper.frame = self.parentViewController.view.bounds; - - _doneButton.frame = CGRectMake(screenEdges.right - _doneButton.frame.size.width - 8.0, screenEdges.top + 2.0, _doneButton.frame.size.width, _doneButton.frame.size.height); - - if (_settingsView != nil) - [_settingsView setInterfaceOrientation:orientation]; - - switch (orientation) - { - case UIInterfaceOrientationLandscapeLeft: - { - _landscapeSettingsView.interfaceOrientation = orientation; - - [UIView performWithoutAnimation:^ - { - _landscapeToolsWrapperView.frame = CGRectMake(0, screenEdges.top, panelToolbarLandscapeSize, _landscapeToolsWrapperView.frame.size.height); - _landscapeSettingsView.frame = CGRectMake(panelToolbarLandscapeSize - TGPhotoPaintBottomPanelSize, 0, TGPhotoPaintBottomPanelSize, _landscapeSettingsView.frame.size.height); - }]; - - _landscapeToolsWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height); - _landscapeSettingsView.frame = CGRectMake(_landscapeSettingsView.frame.origin.x, _landscapeSettingsView.frame.origin.y, _landscapeSettingsView.frame.size.width, _landscapeToolsWrapperView.frame.size.height); - - _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); - _portraitSettingsView.frame = CGRectMake(0, 0, _portraitToolsWrapperView.frame.size.width, TGPhotoPaintBottomPanelSize); - - _landscapeActionsView.frame = CGRectMake(screenEdges.right - TGPhotoPaintTopPanelSize, screenEdges.top, TGPhotoPaintTopPanelSize, referenceSize.height); - - _settingsView.frame = CGRectMake(self.toolbarLandscapeSize + 50.0f + safeAreaInset.left, 0.0f, _settingsView.frame.size.width, _settingsView.frame.size.height); - } - break; - - case UIInterfaceOrientationLandscapeRight: - { - _landscapeSettingsView.interfaceOrientation = orientation; - - [UIView performWithoutAnimation:^ - { - _landscapeToolsWrapperView.frame = CGRectMake(screenSide - panelToolbarLandscapeSize, screenEdges.top, panelToolbarLandscapeSize, _landscapeToolsWrapperView.frame.size.height); - _landscapeSettingsView.frame = CGRectMake(0, 0, TGPhotoPaintBottomPanelSize, _landscapeSettingsView.frame.size.height); - }]; - - _landscapeToolsWrapperView.frame = CGRectMake(screenEdges.right - panelToolbarLandscapeSize, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height); - _landscapeSettingsView.frame = CGRectMake(_landscapeSettingsView.frame.origin.x, _landscapeSettingsView.frame.origin.y, _landscapeSettingsView.frame.size.width, _landscapeToolsWrapperView.frame.size.height); - - _portraitToolsWrapperView.frame = CGRectMake(screenEdges.top, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); - _portraitSettingsView.frame = CGRectMake(0, 0, _portraitToolsWrapperView.frame.size.width, TGPhotoPaintBottomPanelSize); - - _landscapeActionsView.frame = CGRectMake(screenEdges.left, screenEdges.top, TGPhotoPaintTopPanelSize, referenceSize.height); - - _settingsView.frame = CGRectMake(_settingsViewWrapper.frame.size.width - _settingsView.frame.size.width - self.toolbarLandscapeSize - 50.0f - safeAreaInset.right, 0.0f, _settingsView.frame.size.width, _settingsView.frame.size.height); - } - break; - - default: - { - CGFloat x = _landscapeToolsWrapperView.frame.origin.x; - if (x < screenSide / 2) - x = 0; - else - x = screenSide - TGPhotoEditorPanelSize; - _landscapeToolsWrapperView.frame = CGRectMake(x, screenEdges.top, panelToolbarLandscapeSize, referenceSize.height); - - _portraitToolsWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.bottom - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize); - _portraitSettingsView.frame = CGRectMake(0, 0, referenceSize.width, TGPhotoPaintBottomPanelSize); - - _portraitActionsView.frame = CGRectMake(screenEdges.left, screenEdges.top, referenceSize.width, TGPhotoPaintTopPanelSize); - - if ([_context currentSizeClass] == UIUserInterfaceSizeClassRegular) - { - _settingsView.frame = CGRectMake(_settingsViewWrapper.frame.size.width / 2.0f - 10.0f, _settingsViewWrapper.frame.size.height - _settingsView.frame.size.height - TGPhotoEditorToolbarSize - 50.0f, _settingsView.frame.size.width, _settingsView.frame.size.height); - } - else - { - _settingsView.frame = CGRectMake(_settingsViewWrapper.frame.size.width - _settingsView.frame.size.width, _settingsViewWrapper.frame.size.height - _settingsView.frame.size.height - TGPhotoEditorToolbarSize - 50.0f - safeAreaInset.bottom, _settingsView.frame.size.width, _settingsView.frame.size.height); - } - } - break; - } - - PGPhotoEditor *photoEditor = self.photoEditor; - TGPhotoEditorPreviewView *previewView = self.previewView; - - CGSize fittedSize = TGScaleToSize(photoEditor.rotatedCropSize, containerFrame.size); - CGRect previewFrame = CGRectMake((containerFrame.size.width - fittedSize.width) / 2, (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); - - CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight; - CGFloat yCenter = visibleArea / 2.0f; - CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y; - CGFloat offsetHeight = _keyboardHeight > FLT_EPSILON ? offset : 0.0f; - - _wrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2 + offsetHeight, screenSide, screenSide); - - if (_dismissing || (previewView.superview != _containerView && previewView.superview != self.view)) - return; - - if (previewView.superview == self.view) - { - previewFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height); - } - - UIImageOrientation cropOrientation = _photoEditor.cropOrientation; - CGRect cropRect = _photoEditor.cropRect; - CGSize originalSize = _photoEditor.originalSize; - CGFloat rotation = _photoEditor.cropRotation; - - CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); - _contentView.transform = rotationTransform; - _contentView.frame = previewFrame; - - _scrollView.frame = self.view.bounds; - - if (sizeUpdated) { - [self resetScrollView]; - } - [self adjustZoom]; - - _paintingWrapperView.transform = CGAffineTransformMakeRotation(TGRotationForOrientation(cropOrientation)); - _paintingWrapperView.frame = previewFrame; - - CGFloat originalWidth = TGOrientationIsSideward(cropOrientation, NULL) ? previewFrame.size.height : previewFrame.size.width; - CGFloat ratio = originalWidth / cropRect.size.width; - CGRect originalFrame = CGRectMake(-cropRect.origin.x * ratio, -cropRect.origin.y * ratio, originalSize.width * ratio, originalSize.height * ratio); - - previewView.frame = previewFrame; - - if ([self presentedForAvatarCreation]) { - CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForOrientation(photoEditor.cropOrientation)); - if (photoEditor.cropMirrored) - transform = CGAffineTransformScale(transform, -1.0f, 1.0f); - previewView.transform = transform; - } - - CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio); - CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation); - CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f); - - CGFloat scale = fittedOriginalSize.width / _photoEditor.originalSize.width; - CGPoint centerOffset = TGPaintSubtractPoints(centerPoint, [self fittedCropCenterScale:scale]); - - _canvasView.transform = CGAffineTransformIdentity; - _canvasView.frame = originalFrame; - _canvasView.transform = CGAffineTransformMakeRotation(rotation); - _canvasView.center = TGPaintAddPoints(TGPaintCenterOfRect(_paintingWrapperView.bounds), centerOffset); - - _selectionContainerView.transform = CGAffineTransformRotate(rotationTransform, rotation); - _selectionContainerView.frame = previewFrame; - _eyedropperView.frame = _selectionContainerView.bounds; - - _containerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); -} - -#pragma mark - Keyboard Avoidance - -- (void)keyboardWillChangeFrame:(NSNotification *)notification -{ - UIView *parentView = self.view; - - NSTimeInterval duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil ? 0.3 : [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]; - CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - CGRect keyboardFrame = [parentView convertRect:screenKeyboardFrame fromView:nil]; - - CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (parentView.frame.size.height - keyboardFrame.origin.y); - keyboardHeight = MAX(keyboardHeight, 0.0f); - - _keyboardHeight = keyboardHeight; - - [self keyboardHeightChangedTo:keyboardHeight duration:duration curve:curve]; -} - -- (void)keyboardHeightChangedTo:(CGFloat)height duration:(NSTimeInterval)duration curve:(NSInteger)curve -{ - CGSize referenceSize = [self referenceViewSize]; - CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize; - - CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation]; - - CGFloat visibleArea = self.view.frame.size.height - height; - CGFloat yCenter = visibleArea / 2.0f; - CGFloat offset = yCenter - _previewView.center.y - containerFrame.origin.y; - CGFloat offsetHeight = height > FLT_EPSILON ? offset : 0.0f; - - [UIView animateWithDuration:duration delay:0.0 options:curve animations:^ - { - _wrapperView.frame = CGRectMake((referenceSize.width - screenSide) / 2, (referenceSize.height - screenSide) / 2 + offsetHeight, _wrapperView.frame.size.width, _wrapperView.frame.size.height); - _containerView.frame = CGRectMake(containerFrame.origin.x, containerFrame.origin.y + offsetHeight, containerFrame.size.width, containerFrame.size.height); - } completion:nil]; -} - -- (void)_setStickerEntityPosition:(TGPhotoPaintStickerEntity *)entity -{ - TGStickerMaskDescription *mask = [_stickersContext maskDescriptionForDocument:entity.document]; - int64_t documentId = [_stickersContext documentIdForDocument:entity.document]; - TGPhotoMaskPosition *position = [self _positionForMaskDescription:mask documentId:documentId]; - if (position != nil) - { - entity.position = position.center; - entity.angle = position.angle; - entity.scale = position.scale; - } - else - { - entity.position = [self startPositionRelativeToEntity:nil]; - entity.angle = [self startRotation]; - } -} - -- (TGPhotoMaskPosition *)_positionForMaskDescription:(TGStickerMaskDescription *)mask documentId:(int64_t)documentId -{ - if (mask == nil) - return nil; - - TGPhotoMaskAnchor anchor = [TGPhotoMaskPosition anchorOfMask:mask]; - if (anchor == TGPhotoMaskAnchorNone) - return nil; - - TGPaintFace *face = [self _randomFaceWithVacantAnchor:anchor documentId:documentId]; - if (face == nil) - return nil; - - CGPoint referencePoint = CGPointZero; - CGFloat referenceWidth = 0.0f; - CGFloat angle = 0.0f; - CGSize baseSize = [self _stickerBaseSizeForCurrentPainting]; - CGRect faceBounds = [TGPaintFaceUtils transposeRect:face.bounds paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - - switch (anchor) - { - case TGPhotoMaskAnchorForehead: - { - referencePoint = [TGPaintFaceUtils transposePoint:[face foreheadPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - referenceWidth = faceBounds.size.width; - angle = face.angle; - } - break; - - case TGPhotoMaskAnchorEyes: - { - CGPoint point = [face eyesCenterPointAndDistance:&referenceWidth]; - referenceWidth = [TGPaintFaceUtils transposeWidth:referenceWidth paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - referencePoint = [TGPaintFaceUtils transposePoint:point paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - angle = [face eyesAngle]; - } - break; - - case TGPhotoMaskAnchorMouth: - { - referencePoint = [TGPaintFaceUtils transposePoint:[face mouthPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - referenceWidth = faceBounds.size.width; - angle = face.angle; - } - break; - - case TGPhotoMaskAnchorChin: - { - referencePoint = [TGPaintFaceUtils transposePoint:[face chinPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - referenceWidth = faceBounds.size.width; - angle = face.angle; - } - break; - - default: - break; - } - - CGFloat scale = referenceWidth / baseSize.width * mask.zoom; - - CGPoint xComp = CGPointMake(sin(M_PI_2 - angle) * referenceWidth * mask.point.x, - cos(M_PI_2 - angle) * referenceWidth * mask.point.x); - CGPoint yComp = CGPointMake(cos(M_PI_2 + angle) * referenceWidth * mask.point.y, - sin(M_PI_2 + angle) * referenceWidth * mask.point.y); - - CGPoint position = CGPointMake(referencePoint.x + xComp.x + yComp.x, referencePoint.y + xComp.y + yComp.y); - - return [TGPhotoMaskPosition maskPositionWithCenter:position scale:scale angle:angle]; -} - -- (TGPaintFace *)_randomFaceWithVacantAnchor:(TGPhotoMaskAnchor)anchor documentId:(int64_t)documentId -{ - NSInteger randomIndex = (NSInteger)arc4random_uniform((uint32_t)self.faces.count); - NSInteger count = self.faces.count; - NSInteger remaining = self.faces.count; - - for (NSInteger i = randomIndex; remaining > 0; (i = (i + 1) % count), remaining--) - { - TGPaintFace *face = self.faces[i]; - if (![self _isFaceAnchorOccupied:face anchor:anchor documentId:documentId]) - return face; - } - - return nil; -} - -- (bool)_isFaceAnchorOccupied:(TGPaintFace *)face anchor:(TGPhotoMaskAnchor)anchor documentId:(int64_t)documentId -{ - CGPoint anchorPoint = CGPointZero; - switch (anchor) - { - case TGPhotoMaskAnchorForehead: - { - anchorPoint = [TGPaintFaceUtils transposePoint:[face foreheadPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - } - break; - - case TGPhotoMaskAnchorEyes: - { - anchorPoint = [TGPaintFaceUtils transposePoint:[face eyesCenterPointAndDistance:NULL] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - } - break; - - case TGPhotoMaskAnchorMouth: - { - anchorPoint = [TGPaintFaceUtils transposePoint:[face mouthPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - } - break; - - case TGPhotoMaskAnchorChin: - { - anchorPoint = [TGPaintFaceUtils transposePoint:[face chinPoint] paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - } - break; - - default: - { - - } - break; - } - - CGRect faceBounds = [TGPaintFaceUtils transposeRect:face.bounds paintingSize:_painting.size originalSize:_photoEditor.originalSize]; - CGFloat minDistance = faceBounds.size.width * 1.1; - - for (TGPhotoStickerEntityView *view in _entitiesContainerView.subviews) - { - if (![view isKindOfClass:[TGPhotoStickerEntityView class]]) - continue; - - TGPhotoPaintStickerEntity *entity = view.entity; - TGStickerMaskDescription *mask = [_stickersContext maskDescriptionForDocument:view.entity.document]; - int64_t maskDocumentId = [_stickersContext documentIdForDocument:entity.document]; - - if ([TGPhotoMaskPosition anchorOfMask:mask] != anchor) - continue; - - if ((documentId == maskDocumentId || self.faces.count > 1) && TGPaintDistance(entity.position, anchorPoint) < minDistance) - return true; - } - - return false; -} - -- (NSArray *)faces -{ - TGPhotoEditorController *editorController = (TGPhotoEditorController *)self.parentViewController; - if ([editorController isKindOfClass:[TGPhotoEditorController class]]) - return editorController.faces; - else - return @[]; -} - -- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures -{ - return UIRectEdgeTop | UIRectEdgeBottom; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintEntity.m b/submodules/LegacyComponents/Sources/TGPhotoPaintEntity.m deleted file mode 100644 index dd61c0a5efd..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintEntity.m +++ /dev/null @@ -1,27 +0,0 @@ -#import "TGPhotoPaintEntity.h" - -@implementation TGPhotoPaintEntity - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - arc4random_buf(&_uuid, sizeof(NSInteger)); - } - return self; -} - -- (instancetype)copyWithZone:(NSZone *)__unused zone -{ - return nil; -} - -- (instancetype)duplicate -{ - TGPhotoPaintEntity *entity = [self copy]; - arc4random_buf(&entity->_uuid, sizeof(NSInteger)); - return entity; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintEntityView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintEntityView.m deleted file mode 100644 index 17f7d1d52ac..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintEntityView.m +++ /dev/null @@ -1,247 +0,0 @@ -#import "TGPhotoPaintEntityView.h" - -#import "TGPhotoEntitiesContainerView.h" -#import - -const CGFloat TGPhotoPaintEntityMinScale = 0.12f; - -@interface TGPhotoPaintEntityView () -{ - UIPanGestureRecognizer *_panGestureRecognizer; - - bool _measuring; - CGFloat _realScale; - CGAffineTransform _realTransform; -} -@end - -@implementation TGPhotoPaintEntityView - -@dynamic entity; - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.contentScaleFactor = MIN(2.0f, self.contentScaleFactor); - - _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _panGestureRecognizer.delegate = self; - [self addGestureRecognizer:_panGestureRecognizer]; - } - return self; -} - -- (NSInteger)entityUUID -{ - return _entityUUID; -} - -- (void)_pushIdentityTransformForMeasurement -{ - if (_measuring) - return; - - _measuring = true; - _realTransform = self.transform; - _realScale = [[self.layer valueForKeyPath:@"transform.scale.x"] floatValue]; - self.transform = CGAffineTransformIdentity; -} - -- (void)_popIdentityTransformForMeasurement -{ - if (!_measuring) - return; - - _measuring = false; - self.transform = _realTransform; - - _realTransform = CGAffineTransformIdentity; - _realScale = 1.0f; -} - -- (CGFloat)angle -{ - return atan2(self.transform.b, self.transform.a); -} - -- (CGFloat)scale -{ - if (_measuring) - return _realScale; - - return [[self.layer valueForKeyPath:@"transform.scale.x"] floatValue]; -} - -- (void)_notifyOfChange -{ - if (self.entityChanged != nil) - self.entityChanged(self); -} - -- (void)pan:(CGPoint)point absolute:(bool)absolute -{ - if (absolute) - self.center = point; - else - self.center = TGPaintAddPoints(self.center, point); - - [self _notifyOfChange]; -} - -- (void)rotate:(CGFloat)angle absolute:(bool)absolute -{ - CGFloat deltaAngle = angle; - if (absolute) - deltaAngle = angle - self.angle; - self.transform = CGAffineTransformRotate(self.transform, deltaAngle); - - [self _notifyOfChange]; -} - -- (void)scale:(CGFloat)scale absolute:(bool)__unused absolute -{ - CGFloat newScale = self.scale * scale; - - if (newScale < TGPhotoPaintEntityMinScale) - scale = self.scale / TGPhotoPaintEntityMinScale; - - self.transform = CGAffineTransformScale(self.transform, scale, scale); - - [self _notifyOfChange]; -} - -- (bool)inhibitGestures -{ - return _panGestureRecognizer.enabled; -} - -- (void)setInhibitGestures:(bool)inhibitGestures -{ - _panGestureRecognizer.enabled = !inhibitGestures; -} - -- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer -{ - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateBegan: - { - if (self.entityBeganDragging != nil) - self.entityBeganDragging(self); - } - case UIGestureRecognizerStateChanged: - { - CGPoint translation = [gestureRecognizer translationInView:self.superview]; - [self pan:translation absolute:false]; - [gestureRecognizer setTranslation:CGPointZero inView:self.superview]; - } - break; - - case UIGestureRecognizerStateEnded: - { - [self _notifyOfChange]; - } - break; - - default: - break; - } -} - -- (bool)precisePointInside:(CGPoint)point -{ - return [self pointInside:point withEvent:nil]; -} - -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)__unused gestureRecognizer -{ - if (self.shouldTouchEntity != nil) - return self.shouldTouchEntity(self); - - return true; -} - -- (CGRect)selectionBounds -{ - return self.bounds; -} - -- (TGPhotoPaintEntitySelectionView *)createSelectionView -{ - return nil; -} - -- (bool)isTracking -{ - bool panTracking = (_panGestureRecognizer.state == UIGestureRecognizerStateBegan || _panGestureRecognizer.state == UIGestureRecognizerStateChanged); - bool selectionTracking = self.selectionView.isTracking; - - return panTracking || selectionTracking; -} - -@end - - -@implementation TGPhotoPaintEntitySelectionView - -- (void)update -{ - TGPhotoPaintEntityView *entityView = self.entityView; - - [entityView _pushIdentityTransformForMeasurement]; - self.transform = CGAffineTransformIdentity; - CGRect bounds = entityView.selectionBounds; - CGPoint center = TGPaintCenterOfRect(bounds); - - CGFloat scale = [[entityView.superview.superview.layer valueForKeyPath:@"transform.scale.x"] floatValue]; - self.center = [entityView convertPoint:center toView:self.superview]; - self.bounds = CGRectMake(0.0f, 0.0f, bounds.size.width * scale, bounds.size.height * scale); - [entityView _popIdentityTransformForMeasurement]; - - self.transform = CGAffineTransformMakeRotation(entityView.angle); -} - -- (void)fadeIn -{ - self.alpha = 0.0f; - [UIView animateWithDuration:0.18 animations:^ - { - self.alpha = 1.0f; - }]; -} - -- (void)fadeOut -{ - [UIView animateWithDuration:0.18 animations:^ - { - self.alpha = 0.0f; - }]; -} - -@end - -@implementation UIView (PixelColor) - -- (UIColor *)colorAtPoint:(CGPoint)point -{ - if (point.x > self.bounds.size.width || point.y > self.bounds.size.height) - return nil; - - unsigned char pixel[4] = {0}; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast); - - CGContextTranslateCTM(context, -point.x, -point.y); - - [self.layer renderInContext:context]; - - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - - return [UIColor colorWithRed:pixel[0] / 255.0 green:pixel[1] / 255.0 blue:pixel[2] / 255.0 alpha:pixel[3] / 255.0]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.h deleted file mode 100644 index 31317aa742b..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.h +++ /dev/null @@ -1,13 +0,0 @@ -#import - -@interface TGPhotoPaintEyedropperView : UIView - -@property (nonatomic, strong) UIColor *color; -@property (nonatomic, copy) void(^locationChanged)(CGPoint, bool); - -- (void)update; -- (void)present; -- (void)dismiss; - -@end - diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.m deleted file mode 100644 index 2b99e6a9ff9..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintEyedropperView.m +++ /dev/null @@ -1,157 +0,0 @@ -#import "TGPhotoPaintEyedropperView.h" - -#import "TGImageUtils.h" - -@interface TGPhotoPaintEyedropperIndicatorView : UIView - -@property (nonatomic, strong) UIColor *color; - -@end - -@implementation TGPhotoPaintEyedropperIndicatorView - --(instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self != nil) { - self.backgroundColor = [UIColor clearColor]; - self.opaque = false; - self.userInteractionEnabled = false; - } - return self; -} - -- (void)setColor:(UIColor *)color { - _color = color; - [self setNeedsDisplay]; -} - -- (void)drawRect:(CGRect)rect { - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGFloat lineWidth = 1.0f + TGScreenPixel; - - CGContextSetFillColorWithColor(context, _color.CGColor); - CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); - - CGContextSaveGState(context); - - CGContextScaleCTM(context, 0.333333, 0.333333); - CGContextSetLineWidth(context, lineWidth * 3.0); - - TGDrawSvgPath(context, @"M75,0.5 C54.4273931,0.5 35.8023931,8.83869653 22.3205448,22.3205448 C8.83869653,35.8023931 0.5,54.4273931 0.5,75 C0.5,94.6543797 10.7671345,116.856807 23.8111444,136.192682 C42.4188317,163.77591 66.722394,185.676747 75,185.676747 C83.277606,185.676747 107.581168,163.77591 126.188856,136.192682 C139.232866,116.856807 149.5,94.6543797 149.5,75 C149.5,54.4273931 141.161303,35.8023931 127.679455,22.3205448 C114.197607,8.83869653 95.5726069,0.5 75,0.5 Z"); - - TGDrawSvgPath(context, @"M75,0.5 C54.4273931,0.5 35.8023931,8.83869653 22.3205448,22.3205448 C8.83869653,35.8023931 0.5,54.4273931 0.5,75 C0.5,94.6543797 10.7671345,116.856807 23.8111444,136.192682 C42.4188317,163.77591 66.722394,185.676747 75,185.676747 C83.277606,185.676747 107.581168,163.77591 126.188856,136.192682 C139.232866,116.856807 149.5,94.6543797 149.5,75 C149.5,54.4273931 141.161303,35.8023931 127.679455,22.3205448 C114.197607,8.83869653 95.5726069,0.5 75,0.5 S"); - - CGContextRestoreGState(context); - - CGContextSetLineWidth(context, lineWidth); - CGContextFillEllipseInRect(context, CGRectMake(20.0, 68.0, 11.0, 11.0)); - CGContextStrokeEllipseInRect(context, CGRectMake(20.0, 68.0, 11.0, 11.0)); -} - -@end - -@interface TGPhotoPaintEyedropperView() - -@end - -@implementation TGPhotoPaintEyedropperView -{ - TGPhotoPaintEyedropperIndicatorView *_indicatorView; - - UITapGestureRecognizer *_tapGestureRecognizer; - UIPanGestureRecognizer *_panGestureRecognizer; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) { - _indicatorView = [[TGPhotoPaintEyedropperIndicatorView alloc] initWithFrame:CGRectMake(0.0, 0.0, 51.0, 81.0)]; - _indicatorView.layer.anchorPoint = CGPointMake(0.5, 0.92); - [self addSubview:_indicatorView]; - - _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; - [self addGestureRecognizer:_tapGestureRecognizer]; - - _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _panGestureRecognizer.delegate = self; - [self addGestureRecognizer:_panGestureRecognizer]; - } - return self; -} - -- (void)setColor:(UIColor *)color { - _color = color; - _indicatorView.color = color; -} - -- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer { - CGPoint location = [gestureRecognizer locationInView:self]; - [self layoutIndicator:location]; - self.locationChanged(location, true); -} - -- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer { - CGPoint location = [gestureRecognizer locationInView:self]; - switch (gestureRecognizer.state) - { - case UIGestureRecognizerStateChanged: - { - [self layoutIndicator:location]; - self.locationChanged(location, false); - - } - break; - - case UIGestureRecognizerStateEnded: - { - [self layoutIndicator:location]; - self.locationChanged(location, true); - } - break; - - default: - break; - } -} - -- (void)update { - CGPoint location = CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0); - self.locationChanged(location, false); - [self layoutIndicator:location]; -} - -- (void)present { - self.hidden = false; - - _indicatorView.alpha = 0.0f; - _indicatorView.transform = CGAffineTransformMakeScale(0.2, 0.2); - [UIView animateWithDuration:0.2 animations:^ - { - _indicatorView.alpha = 1.0f; - _indicatorView.transform = CGAffineTransformIdentity; - } completion:^(__unused BOOL finished) - { - }]; -} - -- (void)dismiss { - if (self.hidden) - return; - - [UIView animateWithDuration:0.15 animations:^ - { - _indicatorView.alpha = 0.0f; - _indicatorView.transform = CGAffineTransformMakeScale(0.2, 0.2); - } completion:^(__unused BOOL finished) - { - self.hidden = true; - }]; -} - -- (void)layoutIndicator:(CGPoint)point { - _indicatorView.center = CGPointMake(point.x, point.y); -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintFont.h b/submodules/LegacyComponents/Sources/TGPhotoPaintFont.h deleted file mode 100644 index 842f517437b..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintFont.h +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import - -@interface TGPhotoPaintFont : NSObject - -@property (nonatomic, readonly) NSString *title; -@property (nonatomic, readonly) CGFloat titleInset; - -@property (nonatomic, readonly) NSString *fontName; -@property (nonatomic, readonly) NSString *previewFontName; - -@property (nonatomic, readonly) CGFloat sizeCorrection; - -+ (NSArray *)availableFonts; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintFont.m b/submodules/LegacyComponents/Sources/TGPhotoPaintFont.m deleted file mode 100644 index 4e723a8a758..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintFont.m +++ /dev/null @@ -1,36 +0,0 @@ -#import "TGPhotoPaintFont.h" - -@implementation TGPhotoPaintFont - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPhotoPaintFont *font = (TGPhotoPaintFont *)object; - return [font.title isEqualToString:self.title]; -} - -+ (instancetype)fontWithTitle:(NSString *)title titleInset:(CGFloat)titleInset fontName:(NSString *)fontName previewFontName:(NSString *)previewFontName sizeCorrection:(CGFloat)sizeCorrection -{ - TGPhotoPaintFont *font = [[TGPhotoPaintFont alloc] init]; - font->_title = title; - font->_titleInset = titleInset; - font->_fontName = fontName; - font->_previewFontName = previewFontName; - font->_sizeCorrection = sizeCorrection; - return font; -} - -+ (NSArray *)availableFonts -{ - return @ - [ - [TGPhotoPaintFont fontWithTitle:@"Main" titleInset:0 fontName:@"Helvetica-Bold" previewFontName:@"Helvetica" sizeCorrection:0] - ]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.h deleted file mode 100644 index 8db5756e4a8..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface TGPhotoPaintScrollView : UIScrollView - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.m deleted file mode 100644 index 34d70f20867..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintScrollView.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "TGPhotoPaintScrollView.h" - -@implementation TGPhotoPaintScrollView - -- (UIView *)hitTest:(CGPoint)__unused point withEvent:(UIEvent *)__unused event -{ - return nil; -} - -- (void)handlePinch:(id)__unused sender -{ - -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.h deleted file mode 100644 index 5469746bcb5..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.h +++ /dev/null @@ -1,5 +0,0 @@ -#import "TGPhotoEditorSparseView.h" - -@interface TGPhotoPaintSelectionContainerView : TGPhotoEditorSparseView - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.m deleted file mode 100644 index 9b11abb772a..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSelectionContainerView.m +++ /dev/null @@ -1,20 +0,0 @@ -#import "TGPhotoPaintSelectionContainerView.h" - -@implementation TGPhotoPaintSelectionContainerView - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - bool pointInside = [super pointInside:point withEvent:event]; - if (!pointInside) - { - for (UIView *subview in self.subviews) - { - CGPoint convertedPoint = [self convertPoint:point toView:subview]; - if ([subview pointInside:convertedPoint withEvent:event]) - pointInside = true; - } - } - return pointInside; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.h deleted file mode 100644 index 035895cab66..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.h +++ /dev/null @@ -1,47 +0,0 @@ -#import - -#import - -@class TGPaintSwatch; - -typedef enum -{ - TGPhotoPaintSettingsViewIconBrushPen, - TGPhotoPaintSettingsViewIconBrushMarker, - TGPhotoPaintSettingsViewIconBrushNeon, - TGPhotoPaintSettingsViewIconBrushArrow, - TGPhotoPaintSettingsViewIconTextRegular, - TGPhotoPaintSettingsViewIconTextOutlined, - TGPhotoPaintSettingsViewIconTextFramed, - TGPhotoPaintSettingsViewIconMirror -} TGPhotoPaintSettingsViewIcon; - -@interface TGPhotoPaintSettingsView : UIView - -@property (nonatomic, copy) void (^beganColorPicking)(void); -@property (nonatomic, copy) void (^changedColor)(TGPhotoPaintSettingsView *sender, TGPaintSwatch *swatch); -@property (nonatomic, copy) void (^finishedColorPicking)(TGPhotoPaintSettingsView *sender, TGPaintSwatch *swatch); - -@property (nonatomic, copy) void (^eyedropperPressed)(void); -@property (nonatomic, copy) void (^settingsPressed)(void); - -@property (nonatomic, readonly) UIButton *settingsButton; - -@property (nonatomic, strong) TGPaintSwatch *swatch; -@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; - -- (instancetype)initWithContext:(id)context; - -- (void)setIcon:(TGPhotoPaintSettingsViewIcon)icon animated:(bool)animated; -- (void)setHighlighted:(bool)highlighted; - -@end - -@protocol TGPhotoPaintPanelView - -@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; - -- (void)present; -- (void)dismissWithCompletion:(void (^)(void))completion; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.m deleted file mode 100644 index 9a48e7c7c54..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsView.m +++ /dev/null @@ -1,239 +0,0 @@ -#import "TGPhotoPaintSettingsView.h" - -#import "LegacyComponentsInternal.h" -#import "TGImageUtils.h" - -#import "TGPhotoEditorInterfaceAssets.h" - -#import -#import "TGPhotoPaintColorPicker.h" -#import "TGPhotoEditorTintSwatchView.h" - -const CGFloat TGPhotoPaintSettingsPadPickerWidth = 360.0f; - -@interface TGPhotoPaintSettingsView () -{ - TGPhotoPaintColorPicker *_colorPicker; - TGModernButton *_eyedropperButton; - TGModernButton *_settingsButton; - TGPhotoPaintSettingsViewIcon _icon; - - id _context; -} -@end - -@implementation TGPhotoPaintSettingsView - -@dynamic swatch; - -- (instancetype)initWithContext:(id)context -{ - self = [super initWithFrame:CGRectZero]; - if (self) - { - _context = context; - - __weak TGPhotoPaintSettingsView *weakSelf = self; - _colorPicker = [[TGPhotoPaintColorPicker alloc] init]; - _colorPicker.beganPicking = ^ - { - __strong TGPhotoPaintSettingsView *strongSelf = weakSelf; - if (strongSelf != nil && strongSelf.beganColorPicking != nil) - strongSelf.beganColorPicking(); - }; - _colorPicker.valueChanged = ^ - { - __strong TGPhotoPaintSettingsView *strongSelf = weakSelf; - if (strongSelf != nil && strongSelf.changedColor != nil) - strongSelf.changedColor(strongSelf, strongSelf->_colorPicker.swatch); - }; - _colorPicker.finishedPicking = ^ - { - __strong TGPhotoPaintSettingsView *strongSelf = weakSelf; - if (strongSelf != nil && strongSelf.finishedColorPicking != nil) - strongSelf.finishedColorPicking(strongSelf, strongSelf->_colorPicker.swatch); - }; - [self addSubview:_colorPicker]; - - _icon = TGPhotoPaintSettingsViewIconBrushPen; - - _eyedropperButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 44.0f, 44.0f)]; - _eyedropperButton.exclusiveTouch = true; - [_eyedropperButton setImage:TGTintedImage([UIImage imageNamed:@"Editor/Eyedropper"], [UIColor whiteColor]) forState:UIControlStateNormal]; - [_eyedropperButton addTarget:self action:@selector(eyedropperButtonPressed) forControlEvents:UIControlEventTouchUpInside]; -// [self addSubview:_eyedropperButton]; - - _settingsButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 44.0f, 44.0f)]; - _settingsButton.exclusiveTouch = true; - [_settingsButton setImage:[self _imageForIcon:_icon highlighted:false] forState:UIControlStateNormal]; - [_settingsButton addTarget:self action:@selector(settingsButtonPressed) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:_settingsButton]; - } - return self; -} - -- (TGPaintSwatch *)swatch -{ - return _colorPicker.swatch; -} - -- (void)setSwatch:(TGPaintSwatch *)swatch -{ - [_colorPicker setSwatch:swatch]; -} - -- (UIInterfaceOrientation)interfaceOrientation -{ - return _colorPicker.orientation; -} - -- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - _colorPicker.orientation = interfaceOrientation; -} - -- (void)eyedropperButtonPressed -{ - if (self.eyedropperPressed != nil) - self.eyedropperPressed(); -} - -- (void)settingsButtonPressed -{ - if (self.settingsPressed != nil) - self.settingsPressed(); -} - -- (UIButton *)settingsButton -{ - return _settingsButton; -} - -- (void)setIcon:(TGPhotoPaintSettingsViewIcon)icon animated:(bool)animated -{ - void (^changeBlock)(void) = ^ - { - [_settingsButton setImage:[self _imageForIcon:icon highlighted:false] forState:UIControlStateNormal]; - }; - - if (icon == _icon) - return; - - _icon = icon; - - if (animated) - { - UIView *transitionView = [_settingsButton snapshotViewAfterScreenUpdates:false]; - transitionView.frame = _settingsButton.frame; - [_settingsButton.superview addSubview:transitionView]; - - changeBlock(); - _settingsButton.alpha = 0.0f; - _settingsButton.transform = CGAffineTransformMakeScale(0.2f, 0.2); - - [UIView animateWithDuration:0.2 animations:^ - { - transitionView.alpha = 0.0f; - transitionView.transform = CGAffineTransformMakeScale(0.2f, 0.2f); - - _settingsButton.alpha = 1.0f; - _settingsButton.transform = CGAffineTransformIdentity; - } completion:^(__unused BOOL finished) - { - [transitionView removeFromSuperview]; - }]; - } - else - { - changeBlock(); - } -} - -- (void)setHighlighted:(bool)__unused highlighted -{ - [_settingsButton setImage:[self _imageForIcon:_icon highlighted:false] forState:UIControlStateNormal]; -} - -- (UIImage *)_imageForIcon:(TGPhotoPaintSettingsViewIcon)icon highlighted:(bool)highlighted -{ - UIColor *color = highlighted ? [TGPhotoEditorInterfaceAssets accentColor] : [UIColor whiteColor]; - UIImage *iconImage = nil; - switch (icon) - { - case TGPhotoPaintSettingsViewIconBrushPen: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/BrushSelectedPen"], color); - break; - case TGPhotoPaintSettingsViewIconBrushMarker: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/BrushSelectedMarker"], color); - break; - case TGPhotoPaintSettingsViewIconBrushNeon: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/BrushSelectedNeon"], color); - break; - case TGPhotoPaintSettingsViewIconBrushArrow: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/BrushSelectedArrow"], color); - break; - case TGPhotoPaintSettingsViewIconTextRegular: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/TextSelectedRegular"], color); - break; - case TGPhotoPaintSettingsViewIconTextOutlined: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/TextSelectedOutlined"], color); - break; - case TGPhotoPaintSettingsViewIconTextFramed: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/TextSelectedFramed"], color); - break; - case TGPhotoPaintSettingsViewIconMirror: - iconImage = TGTintedImage([UIImage imageNamed:@"Editor/Flip"], color); - break; - } - return iconImage; -} - -+ (NSArray *)colors -{ - static dispatch_once_t onceToken; - static NSArray *colors; - dispatch_once(&onceToken, ^ - { - colors = @ - [ - UIColorRGB(0xfd2a69), - UIColorRGB(0xfe921d), - UIColorRGB(0xfec926), - UIColorRGB(0x67d442), - UIColorRGB(0x1dabf0), - UIColorRGB(0xc273d7), - UIColorRGB(0xffffff), - UIColorRGB(0x282828) - ]; - }); - return colors; -} - -- (void)layoutSubviews -{ - CGFloat leftInset = 23.0f; - CGFloat rightInset = 66.0f; - CGFloat colorPickerHeight = 10.0f; - if (self.frame.size.width > self.frame.size.height) - { - if ([_context currentSizeClass] == UIUserInterfaceSizeClassRegular) - { - _colorPicker.frame = CGRectMake(ceil((self.frame.size.width - TGPhotoPaintSettingsPadPickerWidth) / 2.0f), ceil((self.frame.size.height - colorPickerHeight) / 2.0f), TGPhotoPaintSettingsPadPickerWidth, colorPickerHeight); - _settingsButton.frame = CGRectMake(CGRectGetMaxX(_colorPicker.frame) + 11.0f, floor((self.frame.size.height - _settingsButton.frame.size.height) / 2.0f) + 1.0f, _settingsButton.frame.size.width, _settingsButton.frame.size.height); - } - else - { - _colorPicker.frame = CGRectMake(leftInset, ceil((self.frame.size.height - colorPickerHeight) / 2.0f), self.frame.size.width - leftInset - rightInset, colorPickerHeight); - _eyedropperButton.frame = CGRectMake(10.0f, floor((self.frame.size.height - _eyedropperButton.frame.size.height) / 2.0f) + 1.0f, _eyedropperButton.frame.size.width, _eyedropperButton.frame.size.height); - _settingsButton.frame = CGRectMake(self.frame.size.width - _settingsButton.frame.size.width - 10.0f, floor((self.frame.size.height - _settingsButton.frame.size.height) / 2.0f) + 1.0f, _settingsButton.frame.size.width, _settingsButton.frame.size.height); - } - } - else - { - _colorPicker.frame = CGRectMake(ceil((self.frame.size.width - colorPickerHeight) / 2.0f), rightInset, colorPickerHeight, self.frame.size.height - leftInset - rightInset); - _eyedropperButton.frame = CGRectMake(floor((self.frame.size.width - _eyedropperButton.frame.size.width) / 2.0f), self.frame.size.height - _eyedropperButton.frame.size.height - 10.0, _eyedropperButton.frame.size.width, _eyedropperButton.frame.size.height); - _settingsButton.frame = CGRectMake(floor((self.frame.size.width - _settingsButton.frame.size.width) / 2.0f), 10.0f, _settingsButton.frame.size.width, _settingsButton.frame.size.height); - } -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.h b/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.h deleted file mode 100644 index 89f17b22251..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface TGPhotoPaintSettingsWrapperView : UIButton - -@property (nonatomic, copy) void (^pressed)(CGPoint location); -@property (nonatomic, copy) bool (^suppressTouchAtPoint)(CGPoint location); - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.m b/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.m deleted file mode 100644 index 39ad8b69c6c..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintSettingsWrapperView.m +++ /dev/null @@ -1,24 +0,0 @@ -#import "TGPhotoPaintSettingsWrapperView.h" - -@implementation TGPhotoPaintSettingsWrapperView - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - UIView *view = [super hitTest:point withEvent:event]; - if (view == self) - { - CGPoint location = [self convertPoint:point toView:nil]; - - if (self.pressed != nil) - self.pressed(location); - - if (self.suppressTouchAtPoint != nil && self.suppressTouchAtPoint(location)) - return view; - - return nil; - } - - return view; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintStickerEntity.m b/submodules/LegacyComponents/Sources/TGPhotoPaintStickerEntity.m deleted file mode 100644 index 2b2e463413b..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintStickerEntity.m +++ /dev/null @@ -1,65 +0,0 @@ -#import "TGPhotoPaintStickerEntity.h" - -#import - -@implementation TGPhotoPaintStickerEntity - -@synthesize animated = _animated; - -- (instancetype)initWithDocument:(NSData *)document baseSize:(CGSize)baseSize animated:(bool)animated -{ - self = [super init]; - if (self != nil) - { - _document = document; - _baseSize = baseSize; - _animated = animated; - self.scale = 1.0; - } - return self; -} - -- (instancetype)initWithEmoji:(NSString *)emoji -{ - self = [super init]; - if (self != nil) - { - _emoji = emoji; - _animated = false; - self.scale = 1.0f; - } - return self; -} - -- (instancetype)copyWithZone:(NSZone *)__unused zone -{ - TGPhotoPaintStickerEntity *entity = nil; - if (_document != nil) - entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:self.document baseSize:self.baseSize animated:self.animated]; - else if (_emoji != nil) - entity = [[TGPhotoPaintStickerEntity alloc] initWithEmoji:self.emoji]; - else - return nil; - - entity->_uuid = self.uuid; - entity.position = self.position; - entity.scale = self.scale; - entity.angle = self.angle; - entity.mirrored = self.mirrored; - - return entity; -} - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPhotoPaintStickerEntity *entity = (TGPhotoPaintStickerEntity *)object; - return entity.uuid == self.uuid && CGSizeEqualToSize(entity.baseSize, self.baseSize) && CGPointEqualToPoint(entity.position, self.position) && fabs(entity.scale - self.scale) < FLT_EPSILON && fabs(entity.angle - self.angle) < FLT_EPSILON && entity.mirrored == self.mirrored; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m b/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m deleted file mode 100644 index ead1ac53be8..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoPaintTextEntity.m +++ /dev/null @@ -1,52 +0,0 @@ -#import "TGPhotoPaintTextEntity.h" - -#import "TGPhotoPaintFont.h" -#import "TGPaintSwatch.h" - -@implementation TGPhotoPaintTextEntity - -- (instancetype)initWithText:(NSString *)text font:(TGPhotoPaintFont *)font swatch:(TGPaintSwatch *)swatch baseFontSize:(CGFloat)baseFontSize maxWidth:(CGFloat)maxWidth style:(TGPhotoPaintTextEntityStyle)style -{ - self = [super init]; - if (self != nil) - { - _text = text; - _font = font; - _swatch = swatch; - _baseFontSize = baseFontSize; - _maxWidth = maxWidth; - _style = style; - self.scale = 1.0f; - } - return self; -} - -- (instancetype)copyWithZone:(NSZone *)__unused zone -{ - TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:self.text font:self.font swatch:self.swatch baseFontSize:self.baseFontSize maxWidth:self.maxWidth style:self.style]; - - entity->_uuid = self.uuid; - entity.position = self.position; - entity.scale = self.scale; - entity.angle = self.angle; - - return entity; -} - -- (bool)animated { - return false; -} - -- (BOOL)isEqual:(id)object -{ - if (object == self) - return true; - - if (!object || ![object isKindOfClass:[self class]]) - return false; - - TGPhotoPaintTextEntity *entity = (TGPhotoPaintTextEntity *)object; - return entity.uuid == self.uuid && [entity.text isEqualToString:self.text] && [entity.font isEqual:self.font] && [entity.swatch isEqual:self.swatch] && fabs(entity.baseFontSize - self.baseFontSize) < FLT_EPSILON && fabs(entity.maxWidth - self.maxWidth) < FLT_EPSILON && entity.style == self.style && CGPointEqualToPoint(entity.position, self.position) && fabs(entity.scale - self.scale) < FLT_EPSILON && fabs(entity.angle - self.angle) < FLT_EPSILON && entity.mirrored == self.mirrored; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h deleted file mode 100644 index 92cbdebff12..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.h +++ /dev/null @@ -1,33 +0,0 @@ -#import -#import - -@interface TGPhotoStickerSelectionView : TGPhotoPaintEntitySelectionView - -@end - -@protocol TGPhotoPaintStickersContext; - -@interface TGPhotoStickerEntityView : TGPhotoPaintEntityView - -@property (nonatomic, copy) void(^started)(double); - -@property (nonatomic, readonly) TGPhotoPaintStickerEntity *entity; -@property (nonatomic, readonly) bool isMirrored; - -@property (nonatomic, readonly) int64_t documentId; - -- (instancetype)initWithEntity:(TGPhotoPaintStickerEntity *)entity context:(id)context; -- (void)mirror; -- (UIImage *)image; - -- (void)updateVisibility:(bool)visible; -- (void)seekTo:(double)timestamp; -- (void)play; -- (void)pause; -- (void)resetToStart; -- (void)playFromFrame:(NSInteger)frameIndex; -- (void)copyStickerView:(TGPhotoStickerEntityView *)view; - -- (CGRect)realBounds; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m b/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m deleted file mode 100644 index 0994747edd9..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoStickerEntityView.m +++ /dev/null @@ -1,403 +0,0 @@ -#import "TGPhotoStickerEntityView.h" - -#import "LegacyComponentsInternal.h" - -#import -#import -#import - -#import "TGDocumentMediaAttachment.h" -#import "TGStringUtils.h" -#import "TGImageUtils.h" -#import "TGColor.h" - -const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f; - -@interface UIView (OpaquePixel) - -- (bool)isOpaqueAtPoint:(CGPoint)pixelPoint; - -@end - -@interface TGPhotoStickerSelectionView () -{ - UIView *_leftHandle; - UIView *_rightHandle; - - UIPanGestureRecognizer *_leftGestureRecognizer; - UIPanGestureRecognizer *_rightGestureRecognizer; -} -@end - - -@interface TGPhotoStickerEntityView () -{ - UIView *_stickerView; - - id _document; - bool _animated; - bool _mirrored; - - CGSize _baseSize; - CATransform3D _defaultTransform; -} -@end - -@implementation TGPhotoStickerEntityView - -- (instancetype)initWithEntity:(TGPhotoPaintStickerEntity *)entity context:(id)context -{ - self = [super initWithFrame:CGRectMake(0.0f, 0.0f, entity.baseSize.width, entity.baseSize.height)]; - if (self != nil) - { - _entityUUID = entity.uuid; - _baseSize = entity.baseSize; - _mirrored = entity.isMirrored; - - _stickerView = [context stickerViewForDocument:entity.document]; - - __weak TGPhotoStickerEntityView *weakSelf = self; - _stickerView.started = ^(double duration) { - __strong TGPhotoStickerEntityView *strongSelf = weakSelf; - if (strongSelf != nil && strongSelf.started != nil) - strongSelf.started(duration); - }; - [self addSubview:_stickerView]; - - _document = entity.document; - _animated = entity.animated; - - CGSize imageSize = CGSizeMake(512.0f, 512.0f); - - CGSize displaySize = [self fittedSizeForSize:imageSize maxSize:CGSizeMake(512.0f, 512.0f)]; - - _stickerView.frame = CGRectMake(CGFloor((self.frame.size.width - displaySize.width) / 2.0f), CGFloor((self.frame.size.height - displaySize.height) / 2.0f), displaySize.width, displaySize.height); - - CGFloat scale = displaySize.width > displaySize.height ? self.frame.size.width / displaySize.width : self.frame.size.height / displaySize.height; - _defaultTransform = CATransform3DMakeScale(scale, scale, 1.0f); - _stickerView.layer.transform = _defaultTransform; - - if (_mirrored) - _stickerView.layer.transform = CATransform3DRotate(_defaultTransform, M_PI, 0, 1, 0); - } - return self; -} - - -- (TGPhotoPaintStickerEntity *)entity -{ - TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:_document baseSize:_baseSize animated:_animated]; - entity.uuid = _entityUUID; - entity.position = self.center; - entity.scale = self.scale; - entity.angle = self.angle; - entity.mirrored = _mirrored; - return entity; -} - -- (CGRect)realBounds -{ - CGSize size = CGSizeMake(_baseSize.width * self.scale, _baseSize.height * self.scale); - return CGRectMake(self.center.x - size.width / 2.0f, self.center.y - size.height / 2.0f, size.width, size.height); -} - -- (bool)isMirrored -{ - return _mirrored; -} - -- (CGSize)fittedSizeForSize:(CGSize)size maxSize:(CGSize)maxSize -{ - return TGFitSize(CGSizeMake(size.width, size.height), maxSize); -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)__unused event -{ - CGPoint center = CGPointMake(self.bounds.size.width / 2.0f, self.bounds.size.height / 2.0f); - if (self.selectionView != nil) - { - CGFloat selectionRadius = self.bounds.size.width / sin(M_PI_4); - return pow(point.x - center.x, 2) + pow(point.y - center.y, 2) < pow(selectionRadius / 2.0f, 2); - } - else - { - return [super pointInside:point withEvent:event]; - } -} - -- (bool)precisePointInside:(CGPoint)point -{ - CGPoint imagePoint = [_stickerView convertPoint:point fromView:self]; - if (![_stickerView pointInside:[_stickerView convertPoint:point fromView:self] withEvent:nil]) - return false; - - return [_stickerView isOpaqueAtPoint:imagePoint]; -} - -- (void)mirror -{ - _mirrored = !_mirrored; - - if (iosMajorVersion() >= 7) - { - CATransform3D startTransform = _defaultTransform; - if (!_mirrored) - { - startTransform = _stickerView.layer.transform; - } - CATransform3D targetTransform = CATransform3DRotate(_defaultTransform, 0, 0, 1, 0); - if (_mirrored) - { - targetTransform = CATransform3DRotate(_defaultTransform, M_PI, 0, 1, 0); - targetTransform.m34 = -1.0f / _stickerView.frame.size.width; - } - - [UIView animateWithDuration:0.25 animations:^ - { - _stickerView.layer.transform = targetTransform; - }]; - } - else - { - _stickerView.layer.transform = CATransform3DRotate(_defaultTransform, _mirrored ? M_PI : 0, 0, 1, 0); - } -} - -- (UIImage *)image -{ - return [_stickerView image]; -} - -- (TGPhotoPaintEntitySelectionView *)createSelectionView -{ - TGPhotoStickerSelectionView *view = [[TGPhotoStickerSelectionView alloc] init]; - view.entityView = self; - return view; -} - -- (CGRect)selectionBounds -{ - CGFloat side = self.bounds.size.width / sin(M_PI_4) * self.scale; - return CGRectMake((self.bounds.size.width - side) / 2.0f, (self.bounds.size.height - side) / 2.0f, side, side); -} - -- (void)updateVisibility:(bool)visible { - [_stickerView setIsVisible:visible]; -} - -- (void)seekTo:(double)timestamp { - [_stickerView seekTo:timestamp]; -} - -- (void)play { - [_stickerView play]; -} - -- (void)pause { - [_stickerView pause]; -} - -- (void)resetToStart { - [_stickerView resetToStart]; -} - -- (void)playFromFrame:(NSInteger)frameIndex { - [_stickerView playFromFrame:frameIndex]; -} - -- (void)copyStickerView:(TGPhotoStickerEntityView *)view { - [_stickerView copyStickerView:view->_stickerView]; -} - -- (int64_t)documentId { - return [_stickerView documentId]; -} - -@end - - -@implementation TGPhotoStickerSelectionView - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.backgroundColor = [UIColor clearColor]; - self.contentMode = UIViewContentModeRedraw; - - _leftHandle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TGPhotoStickerSelectionViewHandleSide, TGPhotoStickerSelectionViewHandleSide)]; - [self addSubview:_leftHandle]; - - _leftGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _leftGestureRecognizer.delegate = self; - [_leftHandle addGestureRecognizer:_leftGestureRecognizer]; - - _rightHandle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TGPhotoStickerSelectionViewHandleSide, TGPhotoStickerSelectionViewHandleSide)]; - [self addSubview:_rightHandle]; - - _rightGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _rightGestureRecognizer.delegate = self; - [_rightHandle addGestureRecognizer:_rightGestureRecognizer]; - } - return self; -} - -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer -{ - bool (^isTracking)(UIGestureRecognizer *) = ^bool (UIGestureRecognizer *recognizer) - { - return (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged); - }; - - if (self.entityView.shouldTouchEntity != nil && !self.entityView.shouldTouchEntity(self.entityView)) - return false; - - if (gestureRecognizer == _leftGestureRecognizer) - return !isTracking(_rightGestureRecognizer); - - if (gestureRecognizer == _rightGestureRecognizer) - return !isTracking(_leftGestureRecognizer); - - return true; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - UIView *view = [super hitTest:point withEvent:event]; - if (view == self) - return nil; - - return view; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)__unused event -{ - return CGRectContainsPoint(CGRectInset(self.bounds, -10.0f, -10.0f), point); -} - -- (bool)isTracking -{ - bool (^isTracking)(UIGestureRecognizer *) = ^bool (UIGestureRecognizer *recognizer) - { - return (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged); - }; - - return isTracking(_leftGestureRecognizer) || isTracking(_rightGestureRecognizer); -} - -- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer -{ - CGPoint parentLocation = [gestureRecognizer locationInView:self.superview]; - - if (gestureRecognizer.state == UIGestureRecognizerStateChanged) - { - CGFloat deltaX = [gestureRecognizer translationInView:self].x; - if (gestureRecognizer.view == _leftHandle) - deltaX *= - 1; - CGFloat scaleDelta = (self.bounds.size.width + deltaX * 2) / self.bounds.size.width; - - if (self.entityResized != nil) - self.entityResized(scaleDelta); - - CGFloat angle = 0.0f; - if (gestureRecognizer.view == _leftHandle) - angle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x); - if (gestureRecognizer.view == _rightHandle) - angle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x); - - if (self.entityRotated != nil) - self.entityRotated(angle); - - [gestureRecognizer setTranslation:CGPointZero inView:self]; - } -} - -- (void)drawRect:(CGRect)rect -{ - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGFloat thickness = 1.5f; - CGFloat radius = rect.size.width / 2.0f - 5.5f; - - UIColor *color = UIColorRGBA(0xeaeaea, 0.8); - - CGContextSetFillColorWithColor(context, color.CGColor); - - CGFloat radSpace = TGDegreesToRadians(4.0f); - CGFloat radLen = TGDegreesToRadians(4.0f); - - CGPoint centerPoint = TGPaintCenterOfRect(rect); - - for (NSInteger i = 0; i < 48; i++) - { - CGMutablePathRef path = CGPathCreateMutable(); - - CGPathAddArc(path, NULL, centerPoint.x, centerPoint.y, radius, i * (radSpace + radLen), i * (radSpace + radLen) + radLen, false); - - CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(path, NULL, thickness, kCGLineCapButt, kCGLineJoinMiter, 10); - - CGContextAddPath(context, strokedArc); - - CGPathRelease(strokedArc); - CGPathRelease(path); - } - - CGContextFillPath(context); - - CGContextSetStrokeColorWithColor(context, color.CGColor); - CGContextSetLineWidth(context, thickness); - - void (^drawEllipse)(CGPoint, bool) = ^(CGPoint center, bool clear) - { - CGRect rect = CGRectMake(center.x - 4.5f, center.y - 4.5f, 9.0f, 9.0f); - if (clear) { - rect = CGRectInset(rect, -thickness, -thickness); - CGContextFillEllipseInRect(context, rect); - } else { - CGContextStrokeEllipseInRect(context, rect); - } - }; - - CGContextSetBlendMode(context, kCGBlendModeClear); - - drawEllipse(CGPointMake(5.5f, centerPoint.y), true); - drawEllipse(CGPointMake(rect.size.width - 5.5f, centerPoint.y), true); - - CGContextSetBlendMode(context, kCGBlendModeNormal); - - drawEllipse(CGPointMake(5.5f, centerPoint.y), false); - drawEllipse(CGPointMake(rect.size.width - 5.5f, centerPoint.y), false); -} - -- (void)layoutSubviews -{ - _leftHandle.frame = CGRectMake(-9.5f, floor((self.bounds.size.height - _leftHandle.frame.size.height) / 2.0f), _leftHandle.frame.size.width, _leftHandle.frame.size.height); - _rightHandle.frame = CGRectMake(self.bounds.size.width - _rightHandle.frame.size.width + 9.5f, floor((self.bounds.size.height - _rightHandle.frame.size.height) / 2.0f), _rightHandle.frame.size.width, _rightHandle.frame.size.height); -} - -@end - -@implementation UIView (OpaquePixel) - -- (bool)isOpaqueAtPoint:(CGPoint)point -{ - if (point.x > self.bounds.size.width || point.y > self.bounds.size.height) - return false; - - unsigned char pixel[4] = {0}; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast); - - CGContextTranslateCTM(context, -point.x, -point.y); - - [self.layer renderInContext:context]; - - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - - return pixel[3] > 16; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.h b/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.h deleted file mode 100644 index 2a15e997bf6..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.h +++ /dev/null @@ -1,41 +0,0 @@ -#import "TGPhotoPaintEntityView.h" -#import "TGPhotoPaintTextEntity.h" - -@class TGPaintSwatch; - -@interface TGPhotoTextSelectionView : TGPhotoPaintEntitySelectionView - -@end - - -@interface TGPhotoTextEntityView : TGPhotoPaintEntityView - -@property (nonatomic, readonly) TGPhotoPaintTextEntity *entity; -@property (nonatomic, readonly) UIImage *image; - -@property (nonatomic, readonly) bool isEmpty; - -@property (nonatomic, copy) void (^beganEditing)(TGPhotoTextEntityView *); -@property (nonatomic, copy) void (^finishedEditing)(TGPhotoTextEntityView *); - -- (instancetype)initWithEntity:(TGPhotoPaintTextEntity *)entity; -- (void)setFont:(TGPhotoPaintFont *)font; -- (void)setSwatch:(TGPaintSwatch *)swatch; -- (void)setStyle:(TGPhotoPaintTextEntityStyle)style; - -@property (nonatomic, readonly) bool isEditing; -- (void)beginEditing; -- (void)endEditing; - -@end - - -@interface TGPhotoTextView : UITextView - -@property (nonatomic, strong) UIColor *strokeColor; -@property (nonatomic, assign) CGFloat strokeWidth; -@property (nonatomic, assign) CGPoint strokeOffset; -@property (nonatomic, strong) UIColor *frameColor; -@property (nonatomic, assign) CGFloat frameWidthInset; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m b/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m deleted file mode 100644 index cdce772876e..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoTextEntityView.m +++ /dev/null @@ -1,851 +0,0 @@ -#import "TGPhotoTextEntityView.h" - -#import "TGPaintSwatch.h" -#import "TGPhotoPaintFont.h" - -#import "TGColor.h" -#import "LegacyComponentsInternal.h" - -#import - -const CGFloat TGPhotoTextSelectionViewHandleSide = 30.0f; - -@interface TGPhotoTextView () - -@end - -@interface TGPhotoTextSelectionView () -{ - UIView *_leftHandle; - UIView *_rightHandle; - - UIPanGestureRecognizer *_leftGestureRecognizer; - UIPanGestureRecognizer *_rightGestureRecognizer; -} -@end - - -@interface TGPhotoTextLayoutManager : NSLayoutManager - -@property (nonatomic, strong) UIColor *strokeColor; -@property (nonatomic, assign) CGFloat strokeWidth; -@property (nonatomic, assign) CGPoint strokeOffset; -@property (nonatomic, assign) UIColor *frameColor; -@property (nonatomic, assign) CGFloat frameWidthInset; -@property (nonatomic, assign) CGFloat frameCornerRadius; - -@end - - -@interface TGPhotoTextStorage : NSTextStorage - -@end - - -@interface TGPhotoTextEntityView () -{ - TGPaintSwatch *_swatch; - TGPhotoPaintFont *_font; - CGFloat _baseFontSize; - CGFloat _maxWidth; - TGPhotoPaintTextEntityStyle _style; - - TGPhotoTextView *_textView; -} -@end - -@implementation TGPhotoTextEntityView - -- (instancetype)initWithEntity:(TGPhotoPaintTextEntity *)entity -{ - self = [super initWithFrame:CGRectZero]; - if (self != nil) - { - _entityUUID = entity.uuid; - _baseFontSize = entity.baseFontSize; - _font = entity.font; - _maxWidth = entity.maxWidth; - - _textView = [[TGPhotoTextView alloc] initWithFrame:CGRectZero]; - _textView.clipsToBounds = false; - _textView.backgroundColor = [UIColor clearColor]; - _textView.delegate = self; - _textView.text = entity.text; - _textView.textColor = entity.swatch.color; - _textView.editable = false; - _textView.selectable = false; - _textView.contentInset = UIEdgeInsetsZero; - _textView.showsHorizontalScrollIndicator = false; - _textView.showsVerticalScrollIndicator = false;; - _textView.scrollsToTop = false; - _textView.scrollEnabled = false; - _textView.textContainerInset = UIEdgeInsetsZero; - _textView.textAlignment = NSTextAlignmentCenter; - _textView.minimumZoomScale = 1.0f; - _textView.maximumZoomScale = 1.0f; - _textView.keyboardAppearance = UIKeyboardAppearanceDark; - _textView.autocorrectionType = UITextAutocorrectionTypeNo; - _textView.spellCheckingType = UITextSpellCheckingTypeNo; - _textView.font = [UIFont boldSystemFontOfSize:_baseFontSize]; - _textView.typingAttributes = @{NSFontAttributeName: _textView.font}; -// _textView.frameWidthInset = floor(_baseFontSize * 0.03); - - [self setSwatch:entity.swatch]; - [self setStyle:entity.style]; - - [self addSubview:_textView]; - } - return self; -} - -- (bool)isEmpty -{ - return [_textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length == 0; -} - -- (TGPhotoPaintTextEntity *)entity -{ - TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:_textView.text font:_font swatch:_swatch baseFontSize:_baseFontSize maxWidth:_maxWidth style:_style]; - entity.uuid = _entityUUID; - entity.angle = self.angle; - entity.scale = self.scale; - entity.position = self.center; - - return entity; -} - -- (UIImage *)image { - CGRect rect = self.bounds; - - UIGraphicsBeginImageContextWithOptions(CGSizeMake(rect.size.width, rect.size.height), false, 1.0f); - - [self drawViewHierarchyInRect:rect afterScreenUpdates:false]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return image; -} - -- (bool)isEditing -{ - return _textView.isFirstResponder; -} - -- (void)beginEditing -{ - if (self.beganEditing != nil) - self.beganEditing(self); - - _textView.editable = true; - _textView.selectable = true; - - [_textView.window makeKeyWindow]; - [_textView becomeFirstResponder]; -} - -- (void)endEditing -{ - [_textView resignFirstResponder]; - _textView.editable = false; - _textView.selectable = false; - - _textView.text = [_textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - - if (self.finishedEditing != nil) - self.finishedEditing(self); -} - -#pragma mark - - -- (void)textViewDidChange:(UITextView *)__unused textView -{ - [self sizeToFit]; - [_textView setNeedsDisplay]; -} - -#pragma mark - - -- (void)setSwatch:(TGPaintSwatch *)swatch -{ - _swatch = swatch; - [self updateColor]; -} - -- (void)setFont:(TGPhotoPaintFont *)font -{ - _font = font; - _textView.font = [UIFont boldSystemFontOfSize:_baseFontSize]; - _textView.typingAttributes = @{NSFontAttributeName: _textView.font}; -// _textView.frameWidthInset = floor(_baseFontSize * 0.03); - - [self sizeToFit]; -} - -- (void)setStyle:(TGPhotoPaintTextEntityStyle)style -{ - _style = style; - switch (_style) { - case TGPhotoPaintTextEntityStyleRegular: - _textView.layer.shadowColor = [UIColorRGB(0x000000) CGColor]; - _textView.layer.shadowOffset = CGSizeMake(0.0f, 4.0f); - _textView.layer.shadowOpacity = 0.4f; - _textView.layer.shadowRadius = 4.0f; - break; - - default: - _textView.layer.shadowRadius = 0.0f; - _textView.layer.shadowOpacity = 0.0f; - _textView.layer.shadowOffset = CGSizeMake(0.0f, 0.0f); - _textView.layer.shadowColor = [[UIColor clearColor] CGColor]; - break; - } - - [self updateColor]; - [self setNeedsLayout]; -} - -- (void)updateColor -{ - switch (_style) { - case TGPhotoPaintTextEntityStyleRegular: - { - _textView.textColor = _swatch.color; - _textView.strokeColor = nil; - _textView.frameColor = nil; - } - break; - - case TGPhotoPaintTextEntityStyleOutlined: - { - _textView.textColor = UIColorRGB(0xffffff); - _textView.strokeColor = _swatch.color; - _textView.frameColor = nil; - } - break; - - case TGPhotoPaintTextEntityStyleFramed: - { - CGFloat lightness = 0.0f; - CGFloat r = 0.0f; - CGFloat g = 0.0f; - CGFloat b = 0.0f; - - if ([_swatch.color getRed:&r green:&g blue:&b alpha:NULL]) { - lightness = 0.2126f * r + 0.7152f * g + 0.0722f * b; - } else if ([_swatch.color getWhite:&r alpha:NULL]) { - lightness = r; - } - - if (lightness > 0.87) { - _textView.textColor = UIColorRGB(0x000000); - } else { - _textView.textColor = UIColorRGB(0xffffff); - } - _textView.strokeColor = nil; - _textView.frameColor = _swatch.color; - } - break; - } -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)__unused event -{ - CGFloat x = floor(_textView.font.pointSize / 2.2f); - CGFloat y = floor(_textView.font.pointSize / 3.2f); - if (self.selectionView != nil) - return CGRectContainsPoint(CGRectInset(self.bounds, -(x + 10), -(y + 10)), point); - else - return [_textView pointInside:[_textView convertPoint:point fromView:self] withEvent:nil]; -} - -- (bool)precisePointInside:(CGPoint)point -{ - return [_textView pointInside:[_textView convertPoint:point fromView:self] withEvent:nil]; -} - -- (CGSize)sizeThatFits:(CGSize)__unused size -{ - CGSize result = [_textView sizeThatFits:CGSizeMake(_maxWidth, FLT_MAX)]; - result.width = MAX(224, ceil(result.width) + 20.0f); - result.height = ceil(result.height) + 20.0f + _textView.font.pointSize * _font.sizeCorrection; - return result; -} - -- (void)sizeToFit -{ - CGPoint center = self.center; - CGAffineTransform transform = self.transform; - self.transform = CGAffineTransformIdentity; - [super sizeToFit]; - self.center = center; - self.transform = transform; - - if (self.entityChanged != nil) - self.entityChanged(self); -} - -- (CGRect)selectionBounds -{ - CGFloat x = floor(_textView.font.pointSize / 2.8f); - CGFloat y = floor(_textView.font.pointSize / 4.0f); - CGRect bounds = CGRectInset(self.bounds, -x, -y); - CGSize size = CGSizeMake(bounds.size.width * self.scale, bounds.size.height * self.scale); - return CGRectMake((self.bounds.size.width - size.width) / 2.0f, (self.bounds.size.height - size.height) / 2.0f, size.width, size.height); -} - -- (TGPhotoPaintEntitySelectionView *)createSelectionView -{ - TGPhotoTextSelectionView *view = [[TGPhotoTextSelectionView alloc] init]; - view.entityView = self; - return view; -} - -- (void)layoutSubviews -{ - CGRect rect = self.bounds; - CGFloat correction = _textView.font.pointSize * _font.sizeCorrection; - rect.origin.y += correction; - rect.size.height -= correction; - rect = CGRectOffset(rect, 0.0f, 10.0f); - - _textView.frame = rect; -} - -@end - - -@implementation TGPhotoTextSelectionView - -- (instancetype)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self != nil) - { - self.backgroundColor = [UIColor clearColor]; - self.contentMode = UIViewContentModeRedraw; - - _leftHandle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TGPhotoTextSelectionViewHandleSide, TGPhotoTextSelectionViewHandleSide)]; - [self addSubview:_leftHandle]; - - _leftGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _leftGestureRecognizer.delegate = self; - [_leftHandle addGestureRecognizer:_leftGestureRecognizer]; - - _rightHandle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TGPhotoTextSelectionViewHandleSide, TGPhotoTextSelectionViewHandleSide)]; - [self addSubview:_rightHandle]; - - _rightGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; - _rightGestureRecognizer.delegate = self; - [_rightHandle addGestureRecognizer:_rightGestureRecognizer]; - } - return self; -} - -- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer -{ - bool (^isTracking)(UIGestureRecognizer *) = ^bool (UIGestureRecognizer *recognizer) - { - return (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged); - }; - - if (self.entityView.shouldTouchEntity != nil && !self.entityView.shouldTouchEntity(self.entityView)) - return false; - - if (gestureRecognizer == _leftGestureRecognizer) - return !isTracking(_rightGestureRecognizer); - - if (gestureRecognizer == _rightGestureRecognizer) - return !isTracking(_leftGestureRecognizer); - - return true; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - UIView *view = [super hitTest:point withEvent:event]; - if (view == self) - return nil; - - return view; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)__unused event -{ - return CGRectContainsPoint(CGRectInset(self.bounds, -10.0f, -10.0f), point); -} - -- (bool)isTracking -{ - bool (^isTracking)(UIGestureRecognizer *) = ^bool (UIGestureRecognizer *recognizer) - { - return (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged); - }; - - return isTracking(_leftGestureRecognizer) || isTracking(_rightGestureRecognizer); -} - -- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer -{ - CGPoint parentLocation = [gestureRecognizer locationInView:self.superview]; - - if (gestureRecognizer.state == UIGestureRecognizerStateChanged) - { - CGFloat deltaX = [gestureRecognizer translationInView:self].x; - if (gestureRecognizer.view == _leftHandle) - deltaX *= - 1; - CGFloat scaleDelta = (self.bounds.size.width + deltaX * 2) / self.bounds.size.width; - - if (self.entityResized != nil) - self.entityResized(scaleDelta); - - CGFloat angle = 0.0f; - if (gestureRecognizer.view == _leftHandle) - angle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x); - if (gestureRecognizer.view == _rightHandle) - angle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x); - - if (self.entityRotated != nil) - self.entityRotated(angle); - - [gestureRecognizer setTranslation:CGPointZero inView:self]; - } -} - -- (void)drawRect:(CGRect)rect -{ - CGContextRef context = UIGraphicsGetCurrentContext(); - - CGFloat space = 4.0f; - CGFloat length = 4.5f; - CGFloat thickness = 1.5f; - CGRect selectionBounds = CGRectInset(rect, 5.5f, 5.5f); - - UIColor *color = UIColorRGBA(0xeaeaea, 0.8); - - CGContextSetFillColorWithColor(context, color.CGColor); - - CGPoint centerPoint = TGPaintCenterOfRect(rect); - - NSInteger xCount = (NSInteger)(floor(selectionBounds.size.width / (space + length))); - CGFloat xGap = ceil(((selectionBounds.size.width - xCount * (space + length)) + space) / 2.0f); - for (NSInteger i = 0; i < xCount; i++) - { - CGContextAddRect(context, CGRectMake(xGap + selectionBounds.origin.x + i * (length + space), selectionBounds.origin.y - thickness / 2.0f, length, thickness)); - - CGContextAddRect(context, CGRectMake(xGap + selectionBounds.origin.x + i * (length + space), selectionBounds.origin.y + selectionBounds.size.height - thickness / 2.0f, length, thickness)); - } - - NSInteger yCount = (NSInteger)(floor(selectionBounds.size.height / (space + length))); - CGFloat yGap = ceil(((selectionBounds.size.height - yCount * (space + length)) + space) / 2.0f); - for (NSInteger i = 0; i < yCount; i++) - { - CGContextAddRect(context, CGRectMake(selectionBounds.origin.x - thickness / 2.0f, yGap + selectionBounds.origin.y + i * (length + space), thickness, length)); - - CGContextAddRect(context, CGRectMake(selectionBounds.origin.x + selectionBounds.size.width - thickness / 2.0f, yGap + selectionBounds.origin.y + i * (length + space), thickness, length)); - } - - CGContextFillPath(context); - - CGContextSetStrokeColorWithColor(context, color.CGColor); - CGContextSetLineWidth(context, thickness); - - void (^drawEllipse)(CGPoint, bool) = ^(CGPoint center, bool clear) - { - CGRect rect = CGRectMake(center.x - 4.5f, center.y - 4.5f, 9.0f, 9.0f); - if (clear) { - rect = CGRectInset(rect, -thickness, -thickness); - CGContextFillEllipseInRect(context, rect); - } else { - CGContextStrokeEllipseInRect(context, rect); - } - }; - - CGContextSetBlendMode(context, kCGBlendModeClear); - - drawEllipse(CGPointMake(5.5f, centerPoint.y), true); - drawEllipse(CGPointMake(rect.size.width - 5.5f, centerPoint.y), true); - - CGContextSetBlendMode(context, kCGBlendModeNormal); - - drawEllipse(CGPointMake(5.5f, centerPoint.y), false); - drawEllipse(CGPointMake(rect.size.width - 5.5f, centerPoint.y), false); -} - -- (void)layoutSubviews -{ - _leftHandle.frame = CGRectMake(-9.5f, floor((self.bounds.size.height - _leftHandle.frame.size.height) / 2.0f), _leftHandle.frame.size.width, _leftHandle.frame.size.height); - _rightHandle.frame = CGRectMake(self.bounds.size.width - _rightHandle.frame.size.width + 9.5f, floor((self.bounds.size.height - _rightHandle.frame.size.height) / 2.0f), _rightHandle.frame.size.width, _rightHandle.frame.size.height); -} - -@end - - -@implementation TGPhotoTextView -{ - UIFont *_font; - UIColor *_forcedTextColor; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - TGPhotoTextStorage *textStorage = [[TGPhotoTextStorage alloc] init]; - TGPhotoTextLayoutManager *layoutManager = [[TGPhotoTextLayoutManager alloc] init]; - - NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(0.0f, CGFLOAT_MAX)]; - container.widthTracksTextView = true; - [layoutManager addTextContainer:container]; - [textStorage addLayoutManager:layoutManager]; - - return [self initWithFrame:frame textContainer:container]; -} - -- (CGRect)caretRectForPosition:(UITextPosition *)position -{ - CGRect rect = [super caretRectForPosition:position]; - rect.size.width = rect.size.height / 25.0f; - return rect; -} - -- (CGFloat)strokeWidth -{ - return ((TGPhotoTextLayoutManager *)self.layoutManager).strokeWidth; -} - -- (void)setStrokeWidth:(CGFloat)strokeWidth -{ - [(TGPhotoTextLayoutManager *)self.layoutManager setStrokeWidth:strokeWidth]; - [self setNeedsDisplay]; -} - -- (UIColor *)strokeColor -{ - return ((TGPhotoTextLayoutManager *)self.layoutManager).strokeColor; -} - -- (void)setStrokeColor:(UIColor *)strokeColor -{ - [(TGPhotoTextLayoutManager *)self.layoutManager setStrokeColor:strokeColor]; - [self setNeedsDisplay]; -} - -- (CGPoint)strokeOffset -{ - return ((TGPhotoTextLayoutManager *)self.layoutManager).strokeOffset; -} - -- (void)setStrokeOffset:(CGPoint)strokeOffset -{ - [(TGPhotoTextLayoutManager *)self.layoutManager setStrokeOffset:strokeOffset]; - [self setNeedsDisplay]; -} - -- (UIColor *)frameColor { - return ((TGPhotoTextLayoutManager *)self.layoutManager).frameColor; -} - -- (void)setFrameColor:(UIColor *)frameColor { - [(TGPhotoTextLayoutManager *)self.layoutManager setFrameColor:frameColor]; - [self setNeedsDisplay]; -} - -- (CGFloat)frameWidthInset { - return ((TGPhotoTextLayoutManager *)self.layoutManager).frameWidthInset; -} - -- (void)setFrameWidthInset:(CGFloat)frameWidthInset { - [(TGPhotoTextLayoutManager *)self.layoutManager setFrameWidthInset:frameWidthInset]; - [self setNeedsDisplay]; -} - -- (void)setFont:(UIFont *)font { - [super setFont:font]; - _font = font; - - self.layoutManager.textContainers.firstObject.lineFragmentPadding = floor(font.pointSize * 0.3); -} - -- (void)setTextColor:(UIColor *)textColor { - _forcedTextColor = textColor; - [super setTextColor:textColor]; -} - -- (void)insertText:(NSString *)text { - [self fixTypingAttributes]; - [super insertText:text]; - [self fixTypingAttributes]; -} - -- (void)paste:(id)sender { - [self fixTypingAttributes]; - [super paste:sender]; - [self fixTypingAttributes]; -} - -- (void)fixTypingAttributes { - NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; - if (_font != nil) { - attributes[NSFontAttributeName] = _font; - } - if (_forcedTextColor != nil) { - attributes[NSForegroundColorAttributeName] = _forcedTextColor; - } - self.typingAttributes = attributes; -} - -@end - - -@implementation TGPhotoTextLayoutManager -{ - CGFloat _radius; - NSInteger _maxIndex; - NSArray *_pointArray; - UIBezierPath *_path; - NSMutableArray *_rectArray; -} - -- (instancetype)init { - self = [super init]; - if (self != nil) { - _radius = 8.0f; - } - return self; -} - -- (void)showCGGlyphs:(const CGGlyph *)glyphs positions:(const CGPoint *)positions count:(NSUInteger)glyphCount font:(UIFont *)font matrix:(CGAffineTransform)textMatrix attributes:(NSDictionary *)attributes inContext:(CGContextRef)context -{ - if (self.strokeColor != nil) - { - CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor); - CGContextSetLineJoin(context, kCGLineJoinRound); - - CGFloat lineWidth = self.strokeWidth > FLT_EPSILON ? self.strokeWidth : floor(font.pointSize / 9.0f); - CGContextSetLineWidth(context, lineWidth); - CGContextSetTextDrawingMode(context, kCGTextStroke); - - CGContextSaveGState(context); - CGContextTranslateCTM(context, self.strokeOffset.x, self.strokeOffset.y); - - [super showCGGlyphs:glyphs positions:positions count:glyphCount font:font matrix:textMatrix attributes:attributes inContext:context]; - - CGContextRestoreGState(context); - CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); - CGContextSetTextDrawingMode(context, kCGTextFill); - } - [super showCGGlyphs:glyphs positions:positions count:glyphCount font:font matrix:textMatrix attributes:attributes inContext:context]; -} - -- (void)prepare { - _path = nil; - [self.rectArray removeAllObjects]; - - [self enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.textStorage.string.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { - bool ignoreRange = false; - NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:nil]; - NSString *substring = [[self.textStorage string] substringWithRange:characterRange]; - if ([substring stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]].length == 0) { - ignoreRange = true; - } - - if (!ignoreRange) { - CGRect newRect = CGRectMake(usedRect.origin.x - self.frameWidthInset, usedRect.origin.y, usedRect.size.width + self.frameWidthInset * 2, usedRect.size.height); - NSValue *value = [NSValue valueWithCGRect:newRect]; - [self.rectArray addObject:value]; - } - }]; - - [self preProccess]; -} - -- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { -// [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; - - if (self.frameColor != nil) { - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSaveGState(context); - CGContextTranslateCTM(context, origin.x, origin.y); - - CGContextSetBlendMode(context, kCGBlendModeNormal); - CGContextSetFillColorWithColor(context, self.frameColor.CGColor); - CGContextSetStrokeColorWithColor(context, self.frameColor.CGColor); - - [self prepare]; -// _path = nil; -// [self.rectArray removeAllObjects]; -// -// [self enumerateLineFragmentsForGlyphRange:glyphRange usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { -// bool ignoreRange = false; -// NSString *substring = [[self.textStorage string] substringWithRange:glyphRange]; -// if ([substring stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]].length == 0) { -// ignoreRange = true; -// } -// -// if (!ignoreRange) { -// CGRect newRect = CGRectMake(usedRect.origin.x - self.frameWidthInset, usedRect.origin.y, usedRect.size.width + self.frameWidthInset * 2, usedRect.size.height); -// NSValue *value = [NSValue valueWithCGRect:newRect]; -// [self.rectArray addObject:value]; -// } -// }]; - - [self preProccess]; - - CGRect last = CGRectNull; - for (int i = 0; i < self.rectArray.count; i ++) { - NSValue *curValue = [self.rectArray objectAtIndex:i]; - CGRect cur = curValue.CGRectValue; - _radius = cur.size.height * 0.18; - [self.path appendPath:[UIBezierPath bezierPathWithRoundedRect:cur cornerRadius:_radius]]; - if (i == 0) { - last = cur; - } else if (i > 0 && fabs(CGRectGetMaxY(last) - CGRectGetMinY(cur)) < 10.0) { - CGPoint a = cur.origin; - CGPoint b = CGPointMake(CGRectGetMaxX(cur), cur.origin.y); - CGPoint c = CGPointMake(last.origin.x, CGRectGetMaxY(last)); - CGPoint d = CGPointMake(CGRectGetMaxX(last), CGRectGetMaxY(last)); - - if (a.x - c.x >= 2 * _radius) { - UIBezierPath *addPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(a.x - _radius, a.y + _radius) radius:_radius startAngle:M_PI_2 * 3 endAngle:0 clockwise:YES]; - - [addPath appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(a.x + _radius, a.y + _radius) radius:_radius startAngle:M_PI endAngle:3 * M_PI_2 clockwise:YES]]; - [addPath addLineToPoint:CGPointMake(a.x - _radius, a.y)]; - [self.path appendPath:addPath]; - } - if (a.x == c.x) { - [self.path moveToPoint:CGPointMake(a.x, a.y - _radius)]; - [self.path addLineToPoint:CGPointMake(a.x, a.y + _radius)]; - [self.path addArcWithCenter:CGPointMake(a.x + _radius, a.y + _radius) radius:_radius startAngle:M_PI endAngle:M_PI_2 * 3 clockwise:YES]; - [self.path addArcWithCenter:CGPointMake(a.x + _radius, a.y - _radius) radius:_radius startAngle:M_PI_2 endAngle:M_PI clockwise:YES]; - } - if (d.x - b.x >= 2 * _radius) { - UIBezierPath *addPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(b.x + _radius, b.y + _radius) radius:_radius startAngle:M_PI_2 * 3 endAngle:M_PI clockwise:NO]; - [addPath appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(b.x - _radius, b.y + _radius) radius:_radius startAngle:0 endAngle:3 * M_PI_2 clockwise:NO]]; - [addPath addLineToPoint:CGPointMake(b.x + _radius, b.y)]; - [self.path appendPath:addPath]; - } - if (d.x == b.x) { - [self.path moveToPoint:CGPointMake(b.x, b.y - _radius)]; - [self.path addLineToPoint:CGPointMake(b.x, b.y + _radius)]; - [self.path addArcWithCenter:CGPointMake(b.x - _radius, b.y + _radius) radius:_radius startAngle:0 endAngle:M_PI_2 * 3 clockwise:NO]; - [self.path addArcWithCenter:CGPointMake(b.x - _radius, b.y - _radius) radius:_radius startAngle:M_PI_2 endAngle:0 clockwise:NO]; - } - if (c.x - a.x >= 2 * _radius) { - UIBezierPath *addPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(c.x - _radius, c.y - _radius) radius:_radius startAngle:M_PI_2 endAngle:0 clockwise:NO]; - [addPath appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(c.x + _radius, c.y - _radius) radius:_radius startAngle:M_PI endAngle:M_PI_2 clockwise:NO]]; - [addPath addLineToPoint:CGPointMake(c.x - _radius, c.y)]; - [self.path appendPath:addPath]; - } - if (b.x - d.x >= 2 * _radius) { - UIBezierPath *addPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(d.x + _radius, d.y - _radius) radius:_radius startAngle:M_PI_2 endAngle:M_PI clockwise:YES]; - [addPath appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(d.x - _radius, d.y - _radius) radius:_radius startAngle:0 endAngle:M_PI_2 clockwise:YES]]; - [addPath addLineToPoint:CGPointMake(d.x + _radius, d.y)]; - [self.path appendPath:addPath]; - } - - last = cur; - } - } - [self.path fill]; - [self.path stroke]; - - CGContextRestoreGState(context); - } -} - -- (UIBezierPath *)path { - if (!_path) { - _path = [UIBezierPath bezierPath]; - } - return _path; -} - -- (NSMutableArray *)rectArray { - if (!_rectArray) { - _rectArray = [[NSMutableArray alloc] init]; - } - return _rectArray; -} - -- (void)preProccess { - _maxIndex = 0; - if (self.rectArray.count < 2) { - return; - } - for (int i = 1; i < self.rectArray.count; i++) { - _maxIndex = i; - [self processRectIndex:i]; - } -} - -- (void)processRectIndex:(int) index { - if (self.rectArray.count < 2 || index < 1 || index > _maxIndex) { - return; - } - NSValue *value1 = [self.rectArray objectAtIndex:index - 1]; - NSValue *value2 = [self.rectArray objectAtIndex:index]; - CGRect last = value1.CGRectValue; - CGRect cur = value2.CGRectValue; - _radius = cur.size.height * 0.18; - - BOOL t1 = ((cur.origin.x - last.origin.x < 2 * _radius) && (cur.origin.x > last.origin.x)) || ((CGRectGetMaxX(cur) - CGRectGetMaxX(last) > -2 * _radius) && (CGRectGetMaxX(cur) < CGRectGetMaxX(last))); - BOOL t2 = ((last.origin.x - cur.origin.x < 2 * _radius) && (last.origin.x > cur.origin.x)) || ((CGRectGetMaxX(last) - CGRectGetMaxX(cur) > -2 * _radius) && (CGRectGetMaxX(last) < CGRectGetMaxX(cur))); - - if (t2) { - CGRect newRect = CGRectMake(cur.origin.x, last.origin.y, cur.size.width, last.size.height); - NSValue *newValue = [NSValue valueWithCGRect:newRect]; - [self.rectArray replaceObjectAtIndex:index - 1 withObject:newValue]; - [self processRectIndex:index - 1]; - } - if (t1) { - CGRect newRect = CGRectMake(last.origin.x, cur.origin.y, last.size.width, cur.size.height); - NSValue *newValue = [NSValue valueWithCGRect:newRect]; - [self.rectArray replaceObjectAtIndex:index withObject:newValue]; - [self processRectIndex:index + 1]; - } - return; -} - -@end - - -@implementation TGPhotoTextStorage -{ - NSTextStorage *_impl; -} - -- (instancetype)init -{ - self = [super init]; - - if (self) { - _impl = [NSTextStorage new]; - } - - return self; -} - -- (NSString *)string -{ - return _impl.string; -} - -- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range -{ - return [_impl attributesAtIndex:location effectiveRange:range]; -} - -- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str { - [self beginEditing]; - [_impl replaceCharactersInRange:range withString:str]; - [self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length]; - [self endEditing]; -} - -- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range { - [self beginEditing]; - [_impl setAttributes:attrs range:range]; - [self edited:NSTextStorageEditedAttributes range:range changeInLength:0]; - [self endEditing]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.h b/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.h deleted file mode 100644 index 88817783726..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import "TGPhotoPaintSettingsView.h" -#import "TGPhotoPaintFont.h" -#import "TGPhotoPaintTextEntity.h" - -@interface TGPhotoTextSettingsView : UIView - -@property (nonatomic, copy) void (^fontChanged)(TGPhotoPaintFont *font); -@property (nonatomic, copy) void (^styleChanged)(TGPhotoPaintTextEntityStyle style); - -- (instancetype)initWithFonts:(NSArray *)fonts selectedFont:(TGPhotoPaintFont *)font selectedStyle:(TGPhotoPaintTextEntityStyle)selectedStyle; - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.m b/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.m deleted file mode 100644 index b05b2d5bd29..00000000000 --- a/submodules/LegacyComponents/Sources/TGPhotoTextSettingsView.m +++ /dev/null @@ -1,217 +0,0 @@ -#import "TGPhotoTextSettingsView.h" - -#import "LegacyComponentsInternal.h" -#import "TGImageUtils.h" - -#import "TGPhotoEditorSliderView.h" - -#import -#import "TGPhotoTextEntityView.h" - -const CGFloat TGPhotoTextSettingsViewMargin = 10.0f; -const CGFloat TGPhotoTextSettingsItemHeight = 44.0f; - -@interface TGPhotoTextSettingsView () -{ - NSArray *_fonts; - - UIInterfaceOrientation _interfaceOrientation; - - UIView *_wrapperView; - UIView *_contentView; - UIVisualEffectView *_effectView; - - NSArray *_fontViews; - NSArray *_fontIconViews; - NSArray *_fontSeparatorViews; -} -@end - -@implementation TGPhotoTextSettingsView - -@synthesize interfaceOrientation = _interfaceOrientation; - -- (instancetype)initWithFonts:(NSArray *)fonts selectedFont:(TGPhotoPaintFont *)__unused selectedFont selectedStyle:(TGPhotoPaintTextEntityStyle)selectedStyle -{ - self = [super initWithFrame:CGRectZero]; - if (self) - { - _fonts = fonts; - - _interfaceOrientation = UIInterfaceOrientationPortrait; - - _wrapperView = [[UIView alloc] init]; - _wrapperView.clipsToBounds = true; - _wrapperView.layer.cornerRadius = 12.0; - [self addSubview:_wrapperView]; - - _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]]; - _effectView.alpha = 0.0f; - _effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [_wrapperView addSubview:_effectView]; - - _contentView = [[UIView alloc] init]; - _contentView.alpha = 0.0f; - _contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [_wrapperView addSubview:_contentView]; - - NSMutableArray *fontViews = [[NSMutableArray alloc] init]; - NSMutableArray *fontIconViews = [[NSMutableArray alloc] init]; - NSMutableArray *separatorViews = [[NSMutableArray alloc] init]; - - UIFont *font = [UIFont systemFontOfSize:17]; - - TGModernButton *frameButton = [[TGModernButton alloc] initWithFrame:CGRectZero]; - frameButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - frameButton.titleLabel.font = font; - frameButton.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 16.0f, 0.0f, 0.0f); - frameButton.tag = TGPhotoPaintTextEntityStyleFramed; - [frameButton setTitle:TGLocalized(@"Paint.Framed") forState:UIControlStateNormal]; - [frameButton setTitleColor:[UIColor whiteColor]]; - [frameButton addTarget:self action:@selector(styleValueChanged:) forControlEvents:UIControlEventTouchUpInside]; - [_contentView addSubview:frameButton]; - [fontViews addObject:frameButton]; - - UIImageView *iconView = [[UIImageView alloc] initWithImage:TGTintedImage([UIImage imageNamed:@"Editor/TextFramed"], [UIColor whiteColor])]; - [frameButton addSubview:iconView]; - [fontIconViews addObject:iconView]; - - TGModernButton *outlineButton = [[TGModernButton alloc] initWithFrame:CGRectZero]; - outlineButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - outlineButton.titleLabel.font = font; - outlineButton.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 16.0f, 0.0f, 0.0f); - outlineButton.tag = TGPhotoPaintTextEntityStyleOutlined; - [outlineButton setTitle:TGLocalized(@"Paint.Outlined") forState:UIControlStateNormal]; - [outlineButton setTitleColor:[UIColor whiteColor]]; - [outlineButton addTarget:self action:@selector(styleValueChanged:) forControlEvents:UIControlEventTouchUpInside]; - [_contentView addSubview:outlineButton]; - [fontViews addObject:outlineButton]; - - iconView = [[UIImageView alloc] initWithImage:TGTintedImage([UIImage imageNamed:@"Editor/TextOutlined"], [UIColor whiteColor])]; - [outlineButton addSubview:iconView]; - [fontIconViews addObject:iconView]; - - UIView *separatorView = [[UIView alloc] init]; - separatorView.backgroundColor = UIColorRGBA(0xffffff, 0.2); - [_contentView addSubview:separatorView]; - [separatorViews addObject:separatorView]; - - TGModernButton *regularButton = [[TGModernButton alloc] initWithFrame:CGRectZero]; - regularButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - regularButton.titleLabel.font = font; - regularButton.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 16.0f, 0.0f, 0.0f); - regularButton.tag = TGPhotoPaintTextEntityStyleRegular; - [regularButton setTitle:TGLocalized(@"Paint.Regular") forState:UIControlStateNormal]; - [regularButton setTitleColor:[UIColor whiteColor]]; - [regularButton addTarget:self action:@selector(styleValueChanged:) forControlEvents:UIControlEventTouchUpInside]; - [_contentView addSubview:regularButton]; - [fontViews addObject:regularButton]; - - iconView = [[UIImageView alloc] initWithImage:TGTintedImage([UIImage imageNamed:@"Editor/TextRegular"], [UIColor whiteColor])]; - [regularButton addSubview:iconView]; - [fontIconViews addObject:iconView]; - - separatorView = [[UIView alloc] init]; - separatorView.backgroundColor = UIColorRGBA(0xffffff, 0.2); - [_contentView addSubview:separatorView]; - [separatorViews addObject:separatorView]; - - _fontViews = fontViews; - _fontIconViews = fontIconViews; - _fontSeparatorViews = separatorViews; - } - return self; -} - -- (void)fontButtonPressed:(TGModernButton *)sender -{ - if (self.fontChanged != nil) - self.fontChanged(_fonts[sender.tag]); -} - -- (void)styleValueChanged:(TGModernButton *)sender -{ - if (self.styleChanged != nil) - self.styleChanged((TGPhotoPaintTextEntityStyle)sender.tag); -} - -- (void)present -{ - [UIView animateWithDuration:0.25 animations:^ - { - _effectView.alpha = 1.0f; - _contentView.alpha = 1.0f; - } completion:^(__unused BOOL finished) - { - - }]; -} - -- (void)dismissWithCompletion:(void (^)(void))completion -{ - [UIView animateWithDuration:0.2 animations:^ - { - _effectView.alpha = 0.0f; - _contentView.alpha = 0.0f; - } completion:^(__unused BOOL finished) - { - if (completion != nil) - completion(); - }]; -} - -- (CGSize)sizeThatFits:(CGSize)__unused size -{ - return CGSizeMake(220, _fontViews.count * TGPhotoTextSettingsItemHeight + TGPhotoTextSettingsViewMargin * 2); -} - -- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation -{ - _interfaceOrientation = interfaceOrientation; - - [self setNeedsLayout]; -} - -- (void)layoutSubviews -{ - CGFloat arrowSize = 0.0f; - switch (self.interfaceOrientation) - { - case UIInterfaceOrientationLandscapeLeft: - { - _wrapperView.frame = CGRectMake(TGPhotoTextSettingsViewMargin - arrowSize, TGPhotoTextSettingsViewMargin, self.frame.size.width - TGPhotoTextSettingsViewMargin * 2 + arrowSize, self.frame.size.height - TGPhotoTextSettingsViewMargin * 2); - } - break; - - case UIInterfaceOrientationLandscapeRight: - { - _wrapperView.frame = CGRectMake(TGPhotoTextSettingsViewMargin, TGPhotoTextSettingsViewMargin, self.frame.size.width - TGPhotoTextSettingsViewMargin * 2 + arrowSize, self.frame.size.height - TGPhotoTextSettingsViewMargin * 2); - } - break; - - default: - { - _wrapperView.frame = CGRectMake(TGPhotoTextSettingsViewMargin, TGPhotoTextSettingsViewMargin, self.frame.size.width - TGPhotoTextSettingsViewMargin * 2, self.frame.size.height - TGPhotoTextSettingsViewMargin * 2 + arrowSize); - } - break; - } - - CGFloat thickness = TGScreenPixel; - - [_fontViews enumerateObjectsUsingBlock:^(TGModernButton *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(0.0, TGPhotoTextSettingsItemHeight * index, _contentView.frame.size.width, TGPhotoTextSettingsItemHeight); - }]; - - [_fontIconViews enumerateObjectsUsingBlock:^(UIImageView *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(_contentView.frame.size.width - 42.0f, (TGPhotoTextSettingsItemHeight - view.frame.size.height) / 2.0, view.frame.size.width, view.frame.size.height); - }]; - - [_fontSeparatorViews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) - { - view.frame = CGRectMake(0.0, TGPhotoTextSettingsItemHeight * (index + 1), _contentView.frame.size.width, thickness); - }]; -} - -@end diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index f652cccb86c..bd8f581159e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -25,6 +25,8 @@ @interface TGPhotoToolbarView () UILongPressGestureRecognizer *_longPressGestureRecognizer; bool _transitionedOut; + + bool _animatingCancelDoneButtons; } @end @@ -71,6 +73,9 @@ - (instancetype)initWithContext:(id)context backButton: - (void)setBackButtonType:(TGPhotoEditorBackButton)backButtonType { _backButtonType = backButtonType; + if (_animatingCancelDoneButtons) + return; + UIImage *cancelImage = nil; switch (backButtonType) { @@ -88,6 +93,9 @@ - (void)setBackButtonType:(TGPhotoEditorBackButton)backButtonType { - (void)setDoneButtonType:(TGPhotoEditorDoneButton)doneButtonType { _doneButtonType = doneButtonType; + if (_animatingCancelDoneButtons) + return; + TGMediaAssetsPallete *pallete = nil; if ([_context respondsToSelector:@selector(mediaAssetsPallete)]) pallete = [_context mediaAssetsPallete]; @@ -528,6 +536,74 @@ - (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated } } +- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated { + CGFloat targetAlpha = hidden ? 0.0f : 1.0f; + + if (animated) + { + _animatingCancelDoneButtons = hidden; + if (hidden) { + _cancelButton.modernHighlight = false; + _doneButton.modernHighlight = false; + } + _cancelButton.hidden = false; + _doneButton.hidden = false; + + [UIView animateWithDuration:0.2f + animations:^ + { + _cancelButton.alpha = targetAlpha; + _doneButton.alpha = targetAlpha; + } completion:^(__unused BOOL finished) + { + _animatingCancelDoneButtons = false; + _cancelButton.hidden = hidden; + _doneButton.hidden = hidden; + + if (hidden) { + _cancelButton.modernHighlight = true; + _doneButton.modernHighlight = true; + } + + if (hidden) { + [self setBackButtonType:_backButtonType]; + [self setDoneButtonType:_doneButtonType]; + } + }]; + } + else + { + _cancelButton.alpha = targetAlpha; + _doneButton.alpha = targetAlpha; + _cancelButton.hidden = hidden; + _doneButton.hidden = hidden; + } +} + +- (void)setCenterButtonsHidden:(bool)hidden animated:(bool)animated +{ + CGFloat targetAlpha = hidden ? 0.0f : 1.0f; + + if (animated) + { + _buttonsWrapperView.hidden = false; + + [UIView animateWithDuration:0.2f + animations:^ + { + _buttonsWrapperView.alpha = targetAlpha; + } completion:^(__unused BOOL finished) + { + _buttonsWrapperView.hidden = hidden; + }]; + } + else + { + _buttonsWrapperView.alpha = targetAlpha; + _buttonsWrapperView.hidden = hidden; + } +} + - (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab { for (TGPhotoEditorButton *button in _buttonsWrapperView.subviews) diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolsController.h b/submodules/LegacyComponents/Sources/TGPhotoToolsController.h index 00134a7c5ef..824ff1cb6f2 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolsController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoToolsController.h @@ -3,10 +3,10 @@ @class PGPhotoEditor; @class PGPhotoTool; @class TGPhotoEditorPreviewView; -@class TGPhotoEntitiesContainerView; +@protocol TGPhotoDrawingEntitiesView; @interface TGPhotoToolsController : TGPhotoEditorTabController -- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView; +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView *)entitiesView; @end diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolsController.m b/submodules/LegacyComponents/Sources/TGPhotoToolsController.m index 6a635517e73..f66777e0ae5 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolsController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolsController.m @@ -22,9 +22,8 @@ #import "TGPhotoEditorPreviewView.h" #import "TGPhotoEditorHUDView.h" #import "TGPhotoEditorSparseView.h" -#import "TGPhotoEntitiesContainerView.h" -#import "TGPhotoPaintController.h" +#import "TGPhotoDrawingController.h" const CGFloat TGPhotoEditorToolsPanelSize = 180.0f; const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize + 40.0f; @@ -48,7 +47,7 @@ @interface TGPhotoToolsController () *_entitiesView; void (^_changeBlock)(PGPhotoTool *, id, bool); void (^_interactionBegan)(void); @@ -71,7 +70,7 @@ @interface TGPhotoToolsController () )context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView +- (instancetype)initWithContext:(id)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView *)entitiesView { self = [super initWithContext:context]; if (self != nil) diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 7bbde664ccc..e84c558420e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -12,14 +12,12 @@ @implementation TGPhotoVideoEditor -+ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed ++ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed { id windowManager = [context makeOverlayWindowManager]; id editableItem; - if (image != nil) { - editableItem = image; - } else if (video != nil) { + if (video != nil) { if (![video.path.lowercaseString hasSuffix:@".mp4"]) { NSString *tmpPath = NSTemporaryDirectory(); int64_t fileId = 0; @@ -31,23 +29,37 @@ + (void)presentWithContext:(id)context parentController } editableItem = [[TGCameraCapturedVideo alloc] initWithURL:video]; + } else if (image != nil) { + editableItem = image; } void (^present)(UIImage *) = ^(UIImage *screenImage) { - TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; - // controller.stickersContext = _stickersContext; - controller.skipInitialTransition = true; - controller.dontHideStatusBar = true; - controller.didFinishEditing = ^(__unused id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges) + TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSuggestedAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab]; + controller.senderName = senderName; + controller.stickersContext = stickersContext; + + TGMediaAvatarEditorTransition *transition; + if (transitionView != nil) { + transition = [[TGMediaAvatarEditorTransition alloc] initWithController:controller fromView:transitionView]; + } else { + controller.skipInitialTransition = true; + controller.dontHideStatusBar = true; + } + + controller.didFinishEditing = ^(__unused id adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void)) { if (didFinishWithImage != nil) didFinishWithImage(resultImage); + + commit(); }; - controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges) { + controller.didFinishEditingVideo = ^(AVAsset *asset, id adjustments, UIImage *resultImage, UIImage *thumbnailImage, bool hasChanges, void(^commit)(void)) { if (didFinishWithVideo != nil) { if ([asset isKindOfClass:[AVURLAsset class]]) { didFinishWithVideo(resultImage, [(AVURLAsset *)asset URL], adjustments); } + + commit(); } }; controller.requestThumbnailImage = ^(id editableItem) @@ -80,6 +92,46 @@ + (void)presentWithContext:(id)context parentController TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:controller]; controllerWindow.hidden = false; controller.view.clipsToBounds = true; + + if (transitionView != nil) { + transition.referenceFrame = ^CGRect + { + UIView *referenceView = transitionView; + return [referenceView.superview convertRect:referenceView.frame toView:nil]; + }; + transition.referenceImageSize = ^CGSize + { + return image.size; + }; + transition.referenceScreenImageSignal = ^SSignal * + { + return [SSignal single:image]; + }; + [transition presentAnimated:true]; + + transitionView.alpha = 0.0; + TGDispatchAfter(0.4, dispatch_get_main_queue(), ^{ + transitionView.alpha = 1.0; + }); + + controller.beginCustomTransitionOut = ^(CGRect outReferenceFrame, UIView *repView, void (^completion)(void)) + { + transition.outReferenceFrame = outReferenceFrame; + transition.repView = repView; + + transitionView.alpha = 0.0; + [transition dismissAnimated:true completion:^ + { + transitionView.alpha = 1.0; + dispatch_async(dispatch_get_main_queue(), ^ + { + if (completion != nil) + completion(); + dismissed(); + }); + }]; + }; + } }; if (image != nil) { diff --git a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m index f6b96bb53d8..4c8b2d5e36e 100644 --- a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m +++ b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m @@ -76,33 +76,8 @@ + (instancetype)editAdjustmentsWithDictionary:(NSDictionary *)dictionary } if (dictionary[@"originalSize"]) adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue]; - if (dictionary[@"entities"]) { - NSMutableArray *entities = [[NSMutableArray alloc] init]; - - for (NSDictionary *dict in dictionary[@"entities"]) { - if ([dict[@"type"] isEqualToString:@"sticker"]) { - TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:dict[@"data"] baseSize:[dict[@"baseSize"] CGSizeValue] animated:[dict[@"animated"] boolValue]]; - entity.uuid = [dict[@"uuid"] integerValue]; - entity.position = [dict[@"position"] CGPointValue]; - entity.scale = [dict[@"scale"] floatValue]; - entity.angle = [dict[@"angle"] floatValue]; - entity.mirrored = [dict[@"mirrored"] boolValue]; - [entities addObject:entity]; - } else if ([dict[@"type"] isEqualToString:@"text"]) { - UIImage *renderImage = [[UIImage alloc] initWithData:dict[@"data"]]; - if (renderImage != nil) { - TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:nil font:nil swatch:nil baseFontSize:0.0 maxWidth:0.0 style:TGPhotoPaintTextEntityStyleRegular]; - entity.uuid = [dict[@"uuid"] integerValue]; - entity.position = [dict[@"position"] CGPointValue]; - entity.scale = [dict[@"scale"] floatValue]; - entity.angle = [dict[@"angle"] floatValue]; - entity.renderImage = renderImage; - [entities addObject:entity]; - } - } - } - - adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entities:entities]; + if (dictionary[@"entitiesData"]) { + adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"] hasAnimation:[dictionary[@"hasAnimation"] boolValue] stickers:dictionary[@"stickersData"]]; } else if (dictionary[@"paintingImagePath"]) { adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]]; } @@ -240,42 +215,9 @@ - (NSDictionary *)dictionary if (self.paintingData.imagePath != nil) { dict[@"paintingImagePath"] = self.paintingData.imagePath; } - - NSMutableArray *entities = [[NSMutableArray alloc] init]; - - if (self.paintingData.entities != nil) { - for (TGPhotoPaintEntity *entity in self.paintingData.entities) { - if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) { - TGPhotoPaintStickerEntity *stickerEntity = (TGPhotoPaintStickerEntity *)entity; - NSMutableDictionary *sticker = [[NSMutableDictionary alloc] init]; - sticker[@"type"] = @"sticker"; - sticker[@"baseSize"] = [NSValue valueWithCGSize:stickerEntity.baseSize]; - sticker[@"uuid"] = @(stickerEntity.uuid); - sticker[@"data"] = stickerEntity.document; - sticker[@"position"] = [NSValue valueWithCGPoint:stickerEntity.position]; - sticker[@"scale"] = @(stickerEntity.scale); - sticker[@"angle"] = @(stickerEntity.angle); - sticker[@"mirrored"] = @(stickerEntity.mirrored); - sticker[@"animated"] = @(stickerEntity.animated); - [entities addObject:sticker]; - } else if ([entity isKindOfClass:[TGPhotoPaintTextEntity class]]) { - TGPhotoPaintTextEntity *textEntity = (TGPhotoPaintTextEntity *)entity; - NSMutableDictionary *text = [[NSMutableDictionary alloc] init]; - if (textEntity.renderImage != nil) { - text[@"type"] = @"text"; - text[@"uuid"] = @(textEntity.uuid); - text[@"data"] = UIImagePNGRepresentation(textEntity.renderImage); - text[@"position"] = [NSValue valueWithCGPoint:textEntity.position]; - text[@"scale"] = @(textEntity.scale); - text[@"angle"] = @(textEntity.angle); - [entities addObject:text]; - } - } - } - } - - if (entities.count > 0) { - dict[@"entities"] = entities; + if (self.paintingData.entitiesData != nil) { + dict[@"entitiesData"] = self.paintingData.entitiesData; + dict[@"hasAnimation"] = @(self.paintingData.hasAnimation); } } diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index a5343285966..65aae170219 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/StickerResources:StickerResources", "//submodules/TextFormat:TextFormat", "//submodules/AttachmentUI:AttachmentUI", + "//submodules/DrawingUI:DrawingUI", + "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 2e11b4d5e67..8ea2a99219e 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -58,8 +58,8 @@ public enum LegacyAttachmentMenuMediaEditing { case file } -public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) { - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media) +public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) { + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) |> deliverOnMainQueue).start(next: { (value, isImage) in guard case let .data(data) = value, data.complete else { return @@ -76,13 +76,6 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let recipientName: String @@ -132,7 +125,7 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -195,13 +188,6 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } if canSendImageOrVideo { let carouselItem = TGAttachmentCarouselItemView(context: parentController.context, camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType, saveEditedPhotos: !isSecretChat && saveEditedPhotos, allowGrouping: editMediaOptions == nil && allowGrouping, allowSelection: editMediaOptions == nil, allowEditing: true, document: false, selectionLimit: selectionLimit)! @@ -341,7 +327,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl let editCurrentItem = TGMenuSheetButtonItemView(title: title, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: editCurrentMedia) + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: editCurrentMedia) |> deliverOnMainQueue).start(next: { (value, isImage) in guard case let .data(data) = value, data.complete else { return diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift index dbbb626dc96..d367e03a9d1 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift @@ -2,9 +2,13 @@ import Foundation import UIKit import Display import SwiftSignalKit +import Postbox +import TelegramCore import LegacyComponents import TelegramPresentationData import LegacyUI +import AccountContext +import SaveToCameraRoll public func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, theme: PresentationTheme, present: (ViewController, Any?) -> Void, openCurrent: (() -> Void)?, completion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { _, _, _ in}) { let legacyController = LegacyController(presentation: .custom, theme: theme) @@ -19,7 +23,7 @@ public func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, t present(legacyController, nil) - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: false, hasDeleteButton: false, hasViewButton: openCurrent != nil, personalPhoto: true, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: signup, forum: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: false, hasDeleteButton: false, hasViewButton: openCurrent != nil, personalPhoto: true, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: signup, forum: false, title: nil, isSuggesting: false)! let _ = holder.swap(mixin) mixin.didFinishWithImage = { image in guard let image = image else { @@ -47,3 +51,72 @@ public func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, t } } } + +public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, senderName: String? = nil, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) { + let isVideo = !((media.media as? TelegramMediaImage)?.videoRepresentations.isEmpty ?? true) + + let imageSignal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: false) + |> map { (value, _) -> (UIImage?, Bool) in + if case let .data(data) = value, data.complete { + return (UIImage(contentsOfFile: data.path), true) + } else { + return (nil, false) + } + } + + let videoSignal = isVideo ? fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: true) + |> map { (value, isImage) -> (URL?, Bool) in + if case let .data(data) = value, data.complete && !isImage { + return (URL(fileURLWithPath: data.path), true) + } else { + return (nil, false) + } + } : .single((nil, true)) + + let signals = combineLatest(queue: Queue.mainQueue(), + imageSignal, + videoSignal + ) + |> filter { image, video in + return image.1 && video.1 + } + + let _ = signals.start(next: { image, video in + if image.0 == nil && video.0 == nil { + return + } + + let paintStickersContext = LegacyPaintStickersContext(context: context) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) + legacyController.blocksBackgroundWhenInOverlay = true + legacyController.acceptsFocusWhenInOverlay = true + legacyController.statusBar.statusBarStyle = .Ignore + legacyController.controllerLoaded = { [weak legacyController] in + legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true + } + + let emptyController = LegacyEmptyController(context: legacyController.context)! + emptyController.navigationBarShouldBeHidden = true + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + legacyController.enableSizeClassSignal = true + + present(legacyController, nil) + + TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image.0, video: video.0, stickersContext: paintStickersContext, transitionView: transitionView, senderName: senderName, didFinishWithImage: { image in + if let image = image { + imageCompletion(image) + } + }, didFinishWithVideo: { image, url, adjustments in + if let image = image, let url = url, let adjustments = adjustments { + videoCompletion(image, url, adjustments) + } + }, dismissed: { [weak legacyController] in + legacyController?.dismiss() + }) + }) +} diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index a17d8fba4ec..7afc8c7e12c 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -20,18 +20,11 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" } -public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { +public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { let paintStickersContext = LegacyPaintStickersContext(context: context) paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } controller.captionsEnabled = captionsEnabled controller.inhibitDocumentCaptions = false @@ -199,12 +192,14 @@ private enum LegacyAssetItem { private final class LegacyAssetItemWrapper: NSObject { let item: LegacyAssetItem let timer: Int? + let spoiler: Bool? let groupedId: Int64? let uniqueId: String? - init(item: LegacyAssetItem, timer: Int?, groupedId: Int64?, uniqueId: String?) { + init(item: LegacyAssetItem, timer: Int?, spoiler: Bool?, groupedId: Int64?, uniqueId: String?) { self.item = item self.timer = timer + self.spoiler = spoiler self.groupedId = groupedId self.uniqueId = uniqueId @@ -232,10 +227,10 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let url: String? = (dict["url"] as? String) ?? (dict["url"] as? URL)?.path if let url = url { let dimensions = image.size - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: 4.0), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: false, asAnimation: true, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), thumbnail: thumbnail, caption: caption, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "cloudPhoto" { @@ -256,9 +251,9 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str name = customName } - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .asset(asset.backingAsset), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: nil, spoiler: nil, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), thumbnail: thumbnail, caption: caption, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) } return result } else if (dict["type"] as! NSString) == "file" { @@ -279,12 +274,12 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: tempFileUrl.path, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: nil, caption: caption, asFile: false, asAnimation: true, stickers: []), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), thumbnail: thumbnail, mimeType: mimeType, name: name, caption: caption), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "video" { @@ -296,13 +291,13 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } else if let url = (dict["url"] as? String) ?? (dict["url"] as? URL)?.absoluteString { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } else if (dict["type"] as! NSString) == "cameraVideo" { @@ -318,7 +313,7 @@ public func legacyAssetPickerItemGenerator() -> ((Any?, NSAttributedString?, Str let dimensions = previewImage.pixelSize() let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), thumbnail: thumbnail, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption, asFile: asFile, asAnimation: false, stickers: stickers), timer: (dict["timer"] as? NSNumber)?.intValue, spoiler: (dict["spoiler"] as? NSNumber)?.boolValue, groupedId: (dict["groupedId"] as? NSNumber)?.int64Value, uniqueId: uniqueId) return result } } @@ -337,7 +332,7 @@ public func legacyEnqueueGifMessage(account: Account, data: Data, correlationId: if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var randomId: Int64 = 0 @@ -379,7 +374,7 @@ public func legacyEnqueueVideoMessage(account: Account, data: Data, correlationI if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var randomId: Int64 = 0 @@ -432,7 +427,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } switch data { @@ -446,7 +441,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) var imageFlags: TelegramMediaImageFlags = [] @@ -467,6 +462,9 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } + if let spoiler = item.spoiler, spoiler { + attributes.append(MediaSpoilerMessageAttribute()) + } let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) @@ -502,14 +500,17 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight)) let scaledSize = size.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier, uniqueId: Int64.random(in: Int64.min ... Int64.max)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) var attributes: [MessageAttribute] = [] if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - + if let spoiler = item.spoiler, spoiler { + attributes.append(MediaSpoilerMessageAttribute()) + } + let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) if !entities.isEmpty { @@ -551,7 +552,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } @@ -666,7 +667,7 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - let thumbnailImage = TGScaleImageToPixelSize(thumbnail, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } @@ -751,6 +752,9 @@ public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) - if let timer = item.timer, timer > 0 && timer <= 60 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } + if let spoiler = item.spoiler, spoiler { + attributes.append(MediaSpoilerMessageAttribute()) + } let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift deleted file mode 100644 index 8b22cdf6389..00000000000 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickerView.swift +++ /dev/null @@ -1,187 +0,0 @@ -import UIKit -import Display -import TelegramCore -import AccountContext -import SwiftSignalKit -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import StickerResources -import LegacyComponents - -class LegacyPaintStickerView: UIView, TGPhotoPaintStickerRenderView { - var started: ((Double) -> Void)? - - private let context: AccountContext - private let file: TelegramMediaFile - private var currentSize: CGSize? - private var dimensions: CGSize? - - private let imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? - - private var didSetUpAnimationNode = false - private let stickerFetchedDisposable = MetaDisposable() - - private let cachedDisposable = MetaDisposable() - - init(context: AccountContext, file: TelegramMediaFile) { - self.context = context - self.file = file - - self.imageNode = TransformImageNode() - - super.init(frame: CGRect()) - - self.addSubnode(self.imageNode) - - self.setup() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.stickerFetchedDisposable.dispose() - self.cachedDisposable.dispose() - } - - func image() -> UIImage! { - if self.imageNode.contents != nil { - return UIImage(cgImage: self.imageNode.contents as! CGImage) - } else { - return nil - } - } - - func documentId() -> Int64 { - return self.file.fileId.id - } - - private func setup() { - if let dimensions = self.file.dimensions { - if self.file.isAnimatedSticker || self.file.isVideoSticker { - if self.animationNode == nil { - let animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.autoplay = false - self.animationNode = animationNode - animationNode.started = { [weak self, weak animationNode] in - self?.imageNode.isHidden = true - - if let animationNode = animationNode { - let _ = (animationNode.status - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] status in - self?.started?(status.duration) - }) - } - } - self.addSubnode(animationNode) - } - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: self.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: self.file.resource).start()) - } else { - if let animationNode = self.animationNode { - animationNode.visibility = false - self.animationNode = nil - animationNode.removeFromSupernode() - self.imageNode.isHidden = false - self.didSetUpAnimationNode = false - } - self.imageNode.setSignal(chatMessageSticker(account: self.context.account, file: self.file, small: false, synchronousLoad: false)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: stickerPackFileReference(self.file), resource: chatMessageStickerResource(file: self.file, small: false)).start()) - } - - self.dimensions = dimensions.cgSize - self.setNeedsLayout() - } - } - - var isVisible: Bool = true - func setIsVisible(_ visible: Bool) { - self.isVisible = visible - self.updateVisibility() - } - - var isPlaying = false - func updateVisibility() { - let isPlaying = self.isVisible - if self.isPlaying != isPlaying { - self.isPlaying = isPlaying - - if isPlaying && !self.didSetUpAnimationNode { - self.didSetUpAnimationNode = true - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker) - self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - - self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384) - |> deliverOn(Queue.concurrentDefaultQueue())).start()) - } - self.animationNode?.visibility = isPlaying - } - } - - func seek(to timestamp: Double) { - self.isVisible = false - self.isPlaying = false - self.animationNode?.seekTo(.timestamp(timestamp)) - } - - func play() { - self.isVisible = true - self.updateVisibility() - } - - func pause() { - self.isVisible = false - self.isPlaying = false - self.animationNode?.pause() - } - - func resetToStart() { - self.isVisible = false - self.isPlaying = false - self.animationNode?.seekTo(.timestamp(0.0)) - } - - func play(fromFrame frameIndex: Int) { - self.isVisible = true - self.updateVisibility() - self.animationNode?.play(firstFrame: false, fromIndex: frameIndex) - } - - func copyStickerView(_ view: TGPhotoPaintStickerRenderView!) { - guard let view = view as? LegacyPaintStickerView, let animationNode = view.animationNode else { - return - } - self.animationNode?.cloneCurrentFrame(from: animationNode) - self.animationNode?.play(firstFrame: false, fromIndex: animationNode.currentFrameIndex) - self.updateVisibility() - } - - override func layoutSubviews() { - super.layoutSubviews() - - let size = self.bounds.size - - if size.width > 0 && self.currentSize != size { - self.currentSize = size - - let sideSize: CGFloat = size.width - let boundingSize = CGSize(width: sideSize, height: sideSize) - - if let dimensions = self.dimensions { - let imageSize = dimensions.aspectFitted(boundingSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) - if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) - animationNode.updateLayout(size: imageSize) - } - } - } - } -} diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 638082caa86..336f9326f38 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -8,6 +8,8 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import YuvConversion import StickerResources +import DrawingUI +import SolidRoundedButtonNode protocol LegacyPaintEntity { var position: CGPoint { get } @@ -61,7 +63,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { } var angle: CGFloat { - return self.entity.angle + return self.entity.rotation } var baseSize: CGSize? { @@ -74,7 +76,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { let account: Account let file: TelegramMediaFile - let entity: TGPhotoPaintStickerEntity + let entity: DrawingStickerEntity let animated: Bool let durationPromise = Promise() @@ -90,55 +92,50 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { let imagePromise = Promise() - init?(account: Account, entity: TGPhotoPaintStickerEntity) { - let decoder = PostboxDecoder(buffer: MemoryBuffer(data: entity.document)) - if let file = decoder.decodeRootObject() as? TelegramMediaFile { - self.account = account - self.entity = entity - self.file = file - self.animated = file.isAnimatedSticker || file.isVideoSticker - - if file.isAnimatedSticker || file.isVideoSticker { - self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker) - if let source = self.source { - let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384)) - self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height)) - |> deliverOn(self.queue)).start(next: { [weak self] path, complete in - if let strongSelf = self, complete { - if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { - let queue = strongSelf.queue - let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! - strongSelf.frameCount = frameSource.frameCount - strongSelf.frameRate = frameSource.frameRate - - let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate) - strongSelf.totalDuration = duration - - strongSelf.durationPromise.set(.single(duration)) - - let frameQueue = QueueLocalObject(queue: queue, generate: { - return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) - }) - strongSelf.frameQueue.set(.single(frameQueue)) - } - } - })) - } - } else { - self.disposables.add((chatMessageSticker(account: self.account, file: self.file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) - |> deliverOn(self.queue)).start(next: { [weak self] generator in - if let strongSelf = self { - let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets())) - let image = context?.generateImage() - if let image = image { - strongSelf.imagePromise.set(.single(image)) + init(account: Account, entity: DrawingStickerEntity) { + self.account = account + self.entity = entity + self.file = entity.file + self.animated = file.isAnimatedSticker || file.isVideoSticker + + if file.isAnimatedSticker || file.isVideoSticker { + self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker) + if let source = self.source { + let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384)) + self.disposables.add((source.cachedDataPath(width: Int(fittedDimensions.width), height: Int(fittedDimensions.height)) + |> deliverOn(self.queue)).start(next: { [weak self] path, complete in + if let strongSelf = self, complete { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + let queue = strongSelf.queue + let frameSource = AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! + strongSelf.frameCount = frameSource.frameCount + strongSelf.frameRate = frameSource.frameRate + + let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate) + strongSelf.totalDuration = duration + + strongSelf.durationPromise.set(.single(duration)) + + let frameQueue = QueueLocalObject(queue: queue, generate: { + return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource) + }) + strongSelf.frameQueue.set(.single(frameQueue)) } } })) } } else { - return nil + self.disposables.add((chatMessageSticker(account: self.account, userLocation: .other, file: self.file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) + |> deliverOn(self.queue)).start(next: { [weak self] generator in + if let strongSelf = self { + let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: entity.baseSize, boundingSize: entity.baseSize, intrinsicInsets: UIEdgeInsets())) + let image = context?.generateImage() + if let image = image { + strongSelf.imagePromise.set(.single(image)) + } + } + })) } } @@ -245,7 +242,7 @@ private class LegacyPaintTextEntity: LegacyPaintEntity { } var angle: CGFloat { - return self.entity.angle + return self.entity.rotation } var baseSize: CGSize? { @@ -256,9 +253,129 @@ private class LegacyPaintTextEntity: LegacyPaintEntity { return false } - let entity: TGPhotoPaintTextEntity + let entity: DrawingTextEntity - init(entity: TGPhotoPaintTextEntity) { + init(entity: DrawingTextEntity) { + self.entity = entity + } + + var cachedCIImage: CIImage? + func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) { + var image: CIImage? + if let cachedImage = self.cachedCIImage { + image = cachedImage + } else if let renderImage = entity.renderImage { + image = CIImage(image: renderImage) + self.cachedCIImage = image + } + completion(image) + } +} + +private class LegacyPaintSimpleShapeEntity: LegacyPaintEntity { + var position: CGPoint { + return self.entity.position + } + + var scale: CGFloat { + return 1.0 + } + + var angle: CGFloat { + return self.entity.rotation + } + + var baseSize: CGSize? { + return self.entity.size + } + + var mirrored: Bool { + return false + } + + let entity: DrawingSimpleShapeEntity + + init(entity: DrawingSimpleShapeEntity) { + self.entity = entity + } + + var cachedCIImage: CIImage? + func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) { + var image: CIImage? + if let cachedImage = self.cachedCIImage { + image = cachedImage + } else if let renderImage = entity.renderImage { + image = CIImage(image: renderImage) + self.cachedCIImage = image + } + completion(image) + } +} + +private class LegacyPaintBubbleEntity: LegacyPaintEntity { + var position: CGPoint { + return self.entity.position + } + + var scale: CGFloat { + return 1.0 + } + + var angle: CGFloat { + return self.entity.rotation + } + + var baseSize: CGSize? { + return self.entity.size + } + + var mirrored: Bool { + return false + } + + let entity: DrawingBubbleEntity + + init(entity: DrawingBubbleEntity) { + self.entity = entity + } + + var cachedCIImage: CIImage? + func image(for time: CMTime, fps: Int, completion: @escaping (CIImage?) -> Void) { + var image: CIImage? + if let cachedImage = self.cachedCIImage { + image = cachedImage + } else if let renderImage = entity.renderImage { + image = CIImage(image: renderImage) + self.cachedCIImage = image + } + completion(image) + } +} + +private class LegacyPaintVectorEntity: LegacyPaintEntity { + var position: CGPoint { + return CGPoint(x: self.entity.drawingSize.width * 0.5, y: self.entity.drawingSize.height * 0.5) + } + + var scale: CGFloat { + return 1.0 + } + + var angle: CGFloat { + return 0.0 + } + + var baseSize: CGSize? { + return self.entity.drawingSize + } + + var mirrored: Bool { + return false + } + + let entity: DrawingVectorEntity + + init(entity: DrawingVectorEntity) { self.entity = entity } @@ -288,19 +405,29 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender self.originalSize = adjustments.originalSize self.cropRect = adjustments.cropRect.isEmpty ? nil : adjustments.cropRect - var entities: [LegacyPaintEntity] = [] - if let paintingData = adjustments.paintingData, let paintingEntities = paintingData.entities { - for paintingEntity in paintingEntities { - if let sticker = paintingEntity as? TGPhotoPaintStickerEntity { - if let account = account, let entity = LegacyPaintStickerEntity(account: account, entity: sticker) { - entities.append(entity) + var renderEntities: [LegacyPaintEntity] = [] + if let account = account, let paintingData = adjustments.paintingData, let entitiesData = paintingData.entitiesData { + let entities = decodeDrawingEntities(data: entitiesData) + for entity in entities { + if let sticker = entity as? DrawingStickerEntity { + renderEntities.append(LegacyPaintStickerEntity(account: account, entity: sticker)) + } else if let text = entity as? DrawingTextEntity { + renderEntities.append(LegacyPaintTextEntity(entity: text)) + if let renderSubEntities = text.renderSubEntities { + for entity in renderSubEntities { + renderEntities.append(LegacyPaintStickerEntity(account: account, entity: entity)) + } } - } else if let text = paintingEntity as? TGPhotoPaintTextEntity { - entities.append(LegacyPaintTextEntity(entity: text)) + } else if let simpleShape = entity as? DrawingSimpleShapeEntity { + renderEntities.append(LegacyPaintSimpleShapeEntity(entity: simpleShape)) + } else if let bubble = entity as? DrawingBubbleEntity { + renderEntities.append(LegacyPaintBubbleEntity(entity: bubble)) + } else if let vector = entity as? DrawingVectorEntity { + renderEntities.append(LegacyPaintVectorEntity(entity: vector)) } } } - self.entities = entities + self.entities = renderEntities super.init() } @@ -355,14 +482,14 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } } - public func entities(for time: CMTime, fps: Int, size: CGSize, completion: (([CIImage]?) -> Void)!) { + public func entities(for time: CMTime, fps: Int, size: CGSize, completion: @escaping ([CIImage]) -> Void) { let entities = self.entities let maxSide = max(size.width, size.height) let paintingScale = maxSide / 1920.0 self.queue.async { if entities.isEmpty { - completion(nil) + completion([]) } else { let count = Atomic(value: 1) let images = Atomic<[(CIImage, Int)]>(value: []) @@ -392,7 +519,7 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } transform = CGAffineTransform(translationX: entity.position.x * paintingScale, y: size.height - entity.position.y * paintingScale) - transform = transform.rotated(by: CGFloat.pi * 2 - entity.angle) + transform = transform.rotated(by: CGFloat.pi * 2.0 - entity.angle) transform = transform.scaledBy(x: scale, y: scale) if entity.mirrored { transform = transform.scaledBy(x: -1.0, y: 1.0) @@ -416,8 +543,7 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext { - public var captionPanelView: (() -> TGCaptionPanelView?)! - public var presentStickersController: ((((Any?, Bool, UIView?, CGRect) -> Void)?) -> TGPhotoPaintStickersScreen?)! + public var captionPanelView: (() -> TGCaptionPanelView?)? private let context: AccountContext @@ -425,51 +551,44 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon self.context = context } - public func documentId(forDocument document: Any!) -> Int64 { - if let data = document as? Data{ - let decoder = PostboxDecoder(buffer: MemoryBuffer(data: data)) - if let file = decoder.decodeRootObject() as? TelegramMediaFile { - return file.fileId.id - } else { - return 0 - } - } else { - return 0 + class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter { + let drawingView: TGPhotoDrawingView + let drawingEntitiesView: TGPhotoDrawingEntitiesView + let selectionContainerView: UIView + let contentWrapperView: UIView + let interfaceController: TGPhotoDrawingInterfaceController + + init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { + let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, entitiesView: entitiesView) + self.interfaceController = interfaceController + self.drawingView = interfaceController.drawingView + self.drawingEntitiesView = interfaceController.entitiesView + self.selectionContainerView = interfaceController.selectionContainerView + self.contentWrapperView = interfaceController.contentWrapperView + + super.init() } } - public func maskDescription(forDocument document: Any!) -> TGStickerMaskDescription? { - if let data = document as? Data{ - let decoder = PostboxDecoder(buffer: MemoryBuffer(data: data)) - if let file = decoder.decodeRootObject() as? TelegramMediaFile { - for attribute in file.attributes { - if case let .Sticker(_, _, maskData) = attribute { - if let maskData = maskData { - return TGStickerMaskDescription(n: maskData.n, point: CGPoint(x: maskData.x, y: maskData.y), zoom: CGFloat(maskData.zoom)) - } else { - return nil - } - } - } - return nil - } else { - return nil - } - } else { - return nil - } + public func drawingAdapter(_ size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) -> TGPhotoDrawingAdapter { + return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, entitiesView: entitiesView) } + + public func solidRoundedButton(_ title: String, action: @escaping () -> Void) -> UIView & TGPhotoSolidRoundedButtonView { + let theme = SolidRoundedButtonTheme(theme: self.context.sharedContext.currentPresentationData.with { $0 }.theme) + let button = SolidRoundedButtonView(title: title, theme: theme, height: 50.0, cornerRadius: 10.0) + button.pressed = action + return button + } + + public func drawingEntitiesView(with size: CGSize) -> UIView & TGPhotoDrawingEntitiesView { + let view = DrawingEntitiesView(context: self.context, size: size) + return view + } +} - public func stickerView(forDocument document: Any!) -> (UIView & TGPhotoPaintStickerRenderView)! { - if let data = document as? Data{ - let decoder = PostboxDecoder(buffer: MemoryBuffer(data: data)) - if let file = decoder.decodeRootObject() as? TelegramMediaFile { - return LegacyPaintStickerView(context: self.context, file: file) - } else { - return nil - } - } else { - return nil - } +extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView { + public func updateWidth(_ width: CGFloat) { + let _ = self.updateLayout(width: width, transition: .immediate) } } diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 6cdc49955a4..d40dec056e9 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -109,6 +109,26 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { } } + + public func lockPortrait() { + if let controller = self.controller as? LegacyController { + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + } + + public func unlockPortrait() { + if let controller = self.controller as? LegacyController { + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) + } + } + + public func disableInteractiveKeyboardGesture() { + if let controller = self.controller as? LegacyController { + controller.view.disablesInteractiveModalDismiss = true + controller.view.disablesInteractiveKeyboardGestureRecognizer = true + } + } + public func keyCommandController() -> TGKeyCommandController! { return nil } diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 21ab2b0eb76..7b1ac95cc9f 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -1006,16 +1006,16 @@ public final class ListMessageFileItemNode: ListMessageNode { switch iconImage { case let .imageRepresentation(media, representation): if let file = media as? TelegramMediaFile { - updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, mediaReference: FileMediaReference.message(message: MessageReference(message), media: file).abstract, representation: representation) + updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, userLocation: .peer(message.id.peerId), mediaReference: FileMediaReference.message(message: MessageReference(message), media: file).abstract, representation: representation) } else if let image = media as? TelegramMediaImage { - updateIconImageSignal = mediaGridMessagePhoto(account: item.context.account, photoReference: ImageMediaReference.message(message: MessageReference(message), media: image)) + updateIconImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(message.id.peerId), photoReference: ImageMediaReference.message(message: MessageReference(message), media: image)) } else { updateIconImageSignal = .complete() } case let .albumArt(file, albumArt): updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, engine: item.context.engine, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.presentationData.theme.theme.list.itemAccentColor) case let .roundVideo(file): - updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3)) + updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3)) } } else { updateIconImageSignal = .complete() diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 8e7e91f46ef..3fbdc9dea68 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -578,9 +578,9 @@ public final class ListMessageSnippetItemNode: ListMessageNode { updateIconImageSignal = wallpaperThumbnail(account: item.context.account, accountManager: item.context.sharedContext.accountManager, fileReference: fileReference, wallpaper: previewWallpaper, synchronousLoad: false) } else if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.context.account, photoReference: imageReference) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.context.account, userLocation: (item.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference) } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { - updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1) + updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, userLocation: (item.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1) } } else { updateIconImageSignal = .complete() diff --git a/submodules/LocalizedPeerData/Sources/PeerTitle.swift b/submodules/LocalizedPeerData/Sources/PeerTitle.swift index 341388e51a3..db16fdd65ed 100644 --- a/submodules/LocalizedPeerData/Sources/PeerTitle.swift +++ b/submodules/LocalizedPeerData/Sources/PeerTitle.swift @@ -13,7 +13,7 @@ public extension EnginePeer { } else if let lastName = user.lastName, !lastName.isEmpty { return lastName } else if let _ = user.phone { - return ""// formatPhoneNumber("+\(phone)") + return "" //formatPhoneNumber("+\(phone)") } else { return "" } @@ -46,7 +46,7 @@ public extension EnginePeer { } else if let lastName = user.lastName, !lastName.isEmpty { return lastName } else if let _ = user.phone { - return ""//formatPhoneNumber("+\(phone)") + return "" //formatPhoneNumber("+\(phone)") } else { return strings.User_DeletedAccount } diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index f8d7de74c5f..37dd56c6e64 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/SparseItemGrid:SparseItemGrid", "//submodules/UndoUI:UndoUI", "//submodules/MoreButtonNode:MoreButtonNode", + "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index a1a1b9e4992..82cfcc94efe 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -100,7 +100,7 @@ enum LegacyMediaPickerGallerySource { case selection(item: TGMediaSelectableItem) } -func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController { +func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController { let reminder = peer?.id == context.account.peerId let hasSilentPosting = hasSilentPosting && peer?.id != context.account.peerId @@ -111,17 +111,6 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - if let presentStickers = presentStickers { - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } else { - return nil - } - } let controller = TGModernGalleryController(context: legacyController.context)! controller.asyncTransitionIn = true diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 8d9d91e3db8..0f4059fbe58 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -12,6 +12,9 @@ import Photos import CheckNode import LegacyComponents import PhotoResources +import InvisibleInkDustNode +import ImageBlur +import FastBlur enum MediaPickerGridItemContent: Equatable { case asset(PHFetchResult, Int) @@ -87,6 +90,9 @@ final class MediaPickerGridItemNode: GridItemNode { private var interaction: MediaPickerInteraction? private var theme: PresentationTheme? + private let spoilerDisposable = MetaDisposable() + var spoilerNode: SpoilerOverlayNode? + private var currentIsPreviewing = false var selected: (() -> Void)? @@ -112,6 +118,14 @@ final class MediaPickerGridItemNode: GridItemNode { super.init() self.addSubnode(self.imageNode) + + self.imageNode.contentUpdated = { [weak self] image in + self?.spoilerNode?.setImage(image) + } + } + + deinit { + self.spoilerDisposable.dispose() } var identifier: String { @@ -170,17 +184,20 @@ final class MediaPickerGridItemNode: GridItemNode { let wasHidden = self.isHidden self.isHidden = self.interaction?.hiddenMediaId == self.identifier if !self.isHidden && wasHidden { - self.animateFadeIn(animateCheckNode: true) + self.animateFadeIn(animateCheckNode: true, animateSpoilerNode: true) } } - func animateFadeIn(animateCheckNode: Bool) { + func animateFadeIn(animateCheckNode: Bool, animateSpoilerNode: Bool) { if animateCheckNode { - self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if animateSpoilerNode { + self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func didLoad() { @@ -298,6 +315,31 @@ final class MediaPickerGridItemNode: GridItemNode { } self.imageNode.setSignal(imageSignal) + let spoilerSignal = Signal { subscriber in + if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) { + let disposable = signal.start(next: { next in + if let next = next as? Bool { + subscriber.putNext(next) + } + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + self.spoilerDisposable.set((spoilerSignal + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in + guard let strongSelf = self else { + return + } + strongSelf.updateHasSpoiler(hasSpoiler) + })) + if asset.mediaType == .video { if asset.mediaSubtypes.contains(.videoHighFrameRate) { self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo") @@ -331,6 +373,36 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } + private var didSetupSpoiler = false + private func updateHasSpoiler(_ hasSpoiler: Bool) { + var animated = true + if !self.didSetupSpoiler { + animated = false + self.didSetupSpoiler = true + } + + if hasSpoiler { + if self.spoilerNode == nil { + let spoilerNode = SpoilerOverlayNode() + self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) + self.spoilerNode = spoilerNode + + spoilerNode.setImage(self.imageNode.image) + + if animated { + spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + self.spoilerNode?.update(size: self.bounds.size, transition: .immediate) + self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size) + } else if let spoilerNode = self.spoilerNode { + self.spoilerNode = nil + spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in + spoilerNode?.removeFromSupernode() + }) + } + } + override func layout() { super.layout() @@ -345,6 +417,11 @@ final class MediaPickerGridItemNode: GridItemNode { let checkSize = CGSize(width: 29.0, height: 29.0) self.checkNode?.frame = CGRect(origin: CGPoint(x: self.bounds.width - checkSize.width - 3.0, y: 3.0), size: checkSize) + + if let spoilerNode = self.spoilerNode, self.bounds.width > 0.0 { + spoilerNode.frame = self.bounds + spoilerNode.update(size: self.bounds.size, transition: .immediate) + } } func transitionView() -> UIView { @@ -361,3 +438,81 @@ final class MediaPickerGridItemNode: GridItemNode { } } +class SpoilerOverlayNode: ASDisplayNode { + private let blurNode: ASImageNode + let dustNode: MediaDustNode + + private var maskView: UIView? + private var maskLayer: CAShapeLayer? + + override init() { + self.blurNode = ASImageNode() + self.blurNode.displaysAsynchronously = false + self.blurNode.contentMode = .scaleAspectFill + + self.dustNode = MediaDustNode() + + super.init() + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.blurNode) + self.addSubnode(self.dustNode) + } + + override func didLoad() { + super.didLoad() + + let maskView = UIView() + self.maskView = maskView +// self.dustNode.view.mask = maskView + + let maskLayer = CAShapeLayer() + maskLayer.fillRule = .evenOdd + maskLayer.fillColor = UIColor.white.cgColor + maskView.layer.addSublayer(maskLayer) + self.maskLayer = maskLayer + } + + func setImage(_ image: UIImage?) { + self.blurNode.image = image.flatMap { blurredImage($0) } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.blurNode, frame: CGRect(origin: .zero, size: size)) + + transition.updateFrame(node: self.dustNode, frame: CGRect(origin: .zero, size: size)) + self.dustNode.update(size: size, color: .white, transition: transition) + } +} + +private func blurredImage(_ image: UIImage) -> UIImage? { + guard let image = image.cgImage else { + return nil + } + + let thumbnailSize = CGSize(width: image.width, height: image.height) + let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) + if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) + if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) { + thumbnailContext2.withFlippedContext { c in + c.interpolationQuality = .none + if let image = thumbnailContext.generateImage()?.cgImage { + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) + } + } + imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) + adjustSaturationInContext(context: thumbnailContext2, saturation: 1.7) + return thumbnailContext2.generateImage() + } + } + return nil +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index a1b693acdb5..92c9b856220 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -151,7 +151,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public weak var webSearchController: WebSearchController? public var openCamera: ((TGAttachmentCameraView?) -> Void)? - public var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)? public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } public var presentWebSearch: (MediaGroupsScreen) -> Void = { _ in } @@ -599,8 +598,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } if let node = node { - return (node.view, { [weak node] animateCheckNode in - node?.animateFadeIn(animateCheckNode: animateCheckNode) + return (node.view, node.spoilerNode?.dustNode, { [weak node] animateCheckNode in + node?.animateFadeIn(animateCheckNode: animateCheckNode, animateSpoilerNode: false) }) } else { return nil @@ -681,7 +680,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if let strongSelf = self { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } - }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -717,7 +716,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if let strongSelf = self { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } - }, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -1355,13 +1354,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } if let undoOverlayController = strongSelf.undoOverlayController { - undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, undo: true) + undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true) } else { var elevatedLayout = true if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass { elevatedLayout = false } - let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in + let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in guard let strongSelf = self else { return true } @@ -1498,7 +1497,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if let strongSelf = self { let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, bannedSendMedia: strongSelf.bannedSendMedia, subject: .assets(collection), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState) - mediaPicker.presentStickers = strongSelf.presentStickers mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker mediaPicker.getCaptionPanelView = strongSelf.getCaptionPanelView @@ -1516,21 +1514,40 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let strings = self.presentationData.strings let selectionCount = self.selectionCount + var isSpoilerAvailable = true + if let peer = self.peer, case .secretChat = peer { + isSpoilerAvailable = false + } + + var hasSpoilers = false + var hasGeneric = false + if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if editingContext.spoiler(for: item) { + hasSpoilers = true + } else { + hasGeneric = true + } + } + } + let items: Signal = self.groupedPromise.get() |> deliverOnMainQueue |> map { [weak self] grouped -> ContextController.Items in var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {}) - }))) - + if !hasSpoilers { + items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {}) + }))) + } if selectionCount > 1 { - items.append(.separator) - + if !items.isEmpty { + items.append(.separator) + } items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in if !grouped { return nil @@ -1552,7 +1569,23 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self?.groupedValue = false }))) } - + if isSpoilerAvailable { + if !items.isEmpty { + items.append(.separator) + } + items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, animationName: "anim_spoiler", action: { [weak self] _, f in + f(.default) + guard let strongSelf = self else { + return + } + + if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + editingContext.setSpoiler(hasGeneric, for: item) + } + } + }))) + } return ContextController.Items(content: .list(items)) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 5b2cc835541..9035dd281e1 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -25,6 +25,9 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { private var adjustmentsDisposable: Disposable? + private let spoilerDisposable = MetaDisposable() + private var spoilerNode: SpoilerOverlayNode? + private var theme: PresentationTheme? private var validLayout: CGSize? @@ -68,43 +71,75 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.addSubnode(self.imageNode) - if asset.isVideo, let editingState = interaction?.editingState { - func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal { - return Signal { subscriber in - let disposable = editingState.adjustmentsSignal(for: asset).start(next: { next in - if let next = next as? TGMediaEditAdjustments { - subscriber.putNext(next) - } else if next == nil { - subscriber.putNext(nil) + if let editingState = interaction?.editingState { + if asset.isVideo { + func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal { + return Signal { subscriber in + let disposable = editingState.adjustmentsSignal(for: asset).start(next: { next in + if let next = next as? TGMediaEditAdjustments { + subscriber.putNext(next) + } else if next == nil { + subscriber.putNext(nil) + } + }, error: nil, completed: {}) + return ActionDisposable { + disposable?.dispose() } - }, error: nil, completed: {}) - return ActionDisposable { - disposable?.dispose() } } + + self.adjustmentsDisposable = (adjustmentsChangedSignal(editingState: editingState) + |> deliverOnMainQueue).start(next: { [weak self] adjustments in + if let strongSelf = self { + let duration: Double + if let adjustments = adjustments as? TGVideoEditAdjustments, adjustments.trimApplied() { + duration = adjustments.trimEndValue - adjustments.trimStartValue + } else { + duration = asset.originalDuration ?? 0.0 + } + strongSelf.videoDuration = duration + + if let size = strongSelf.validLayout { + strongSelf.updateLayout(size: size, transition: .immediate) + } + } + }) } - self.adjustmentsDisposable = (adjustmentsChangedSignal(editingState: editingState) - |> deliverOnMainQueue).start(next: { [weak self] adjustments in - if let strongSelf = self { - let duration: Double - if let adjustments = adjustments as? TGVideoEditAdjustments, adjustments.trimApplied() { - duration = adjustments.trimEndValue - adjustments.trimStartValue - } else { - duration = asset.originalDuration ?? 0.0 - } - strongSelf.videoDuration = duration + let spoilerSignal = Signal { subscriber in + if let signal = editingState.spoilerSignal(forIdentifier: asset.uniqueIdentifier) { + let disposable = signal.start(next: { next in + if let next = next as? Bool { + subscriber.putNext(next) + } + }, error: { _ in + }, completed: nil)! - if let size = strongSelf.validLayout { - strongSelf.updateLayout(size: size, transition: .immediate) + return ActionDisposable { + disposable.dispose() } + } else { + return EmptyDisposable } - }) + } + + self.spoilerDisposable.set((spoilerSignal + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in + guard let strongSelf = self else { + return + } + strongSelf.updateHasSpoiler(hasSpoiler) + })) + } + + self.imageNode.contentUpdated = { [weak self] image in + self?.spoilerNode?.setImage(image) } } deinit { self.adjustmentsDisposable?.dispose() + self.spoilerDisposable.dispose() } override func didLoad() { @@ -120,6 +155,36 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.interaction?.openSelectedMedia(asset, self.imageNode.image) } + private var didSetupSpoiler = false + private func updateHasSpoiler(_ hasSpoiler: Bool) { + var animated = true + if !self.didSetupSpoiler { + animated = false + self.didSetupSpoiler = true + } + + if hasSpoiler { + if self.spoilerNode == nil { + let spoilerNode = SpoilerOverlayNode() + self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) + self.spoilerNode = spoilerNode + + spoilerNode.setImage(self.imageNode.image) + + if animated { + spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + self.spoilerNode?.update(size: self.bounds.size, transition: .immediate) + self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size) + } else if let spoilerNode = self.spoilerNode { + self.spoilerNode = nil + spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in + spoilerNode?.removeFromSupernode() + }) + } + } + func setup(size: CGSize) { let editingState = self.interaction?.editingState let editedSignal = Signal { subscriber in @@ -220,14 +285,18 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.isHidden = self.interaction?.hiddenMediaId == asset.uniqueIdentifier if !self.isHidden && wasHidden { if let checkNode = self.checkNode, checkNode.alpha > 0.0 { - checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } if let durationTextNode = self.durationTextNode, durationTextNode.alpha > 0.0 { - durationTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + durationTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } if let durationBackgroundNode = self.durationBackgroundNode, durationBackgroundNode.alpha > 0.0 { - durationBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + durationBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + if let spoilerNode = self.spoilerNode, spoilerNode.alpha > 0.0 { + spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } } @@ -249,6 +318,11 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + if let spoilerNode = self.spoilerNode { + transition.updateFrame(node: spoilerNode, frame: CGRect(origin: CGPoint(), size: size)) + spoilerNode.update(size: size, transition: transition) + } + let checkSize = CGSize(width: 29.0, height: 29.0) if let checkNode = self.checkNode { transition.updateFrame(node: checkNode, frame: CGRect(origin: CGPoint(x: size.width - checkSize.width - 3.0, y: 3.0), size: checkSize)) @@ -320,7 +394,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { }) } - func animateTo(_ view: UIView, completion: @escaping (Bool) -> Void) { + func animateTo(_ view: UIView, dustNode: ASDisplayNode?, completion: @escaping (Bool) -> Void) { view.alpha = 0.0 let frame = self.frame @@ -331,6 +405,20 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.durationTextNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + var dustSupernode: ASDisplayNode? + var dustPosition: CGPoint? + if let dustNode = dustNode { + dustSupernode = dustNode.supernode + dustPosition = dustNode.position + + self.addSubnode(dustNode) + dustNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + dustNode.layer.animatePosition(from: CGPoint(x: frame.width / 2.0, y: frame.height / 2.0), to: dustNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + self.spoilerNode?.dustNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + self.corners = [] self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring)) self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in @@ -339,6 +427,11 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self?.durationTextNode?.layer.removeAllAnimations() self?.durationBackgroundNode?.layer.removeAllAnimations() + if let dustNode = dustNode { + dustSupernode?.addSubnode(dustNode) + dustNode.position = dustPosition ?? dustNode.position + } + var animateCheckNode = false if let strongSelf = self, let checkNode = strongSelf.checkNode, checkNode.alpha.isZero { animateCheckNode = true @@ -350,6 +443,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { Queue.mainQueue().after(0.01) { self?.layer.removeAllAnimations() + self?.spoilerNode?.dustNode.layer.removeAllAnimations() } }) } @@ -479,7 +573,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI }) } - var getTransitionView: (String ) -> (UIView, (Bool) -> Void)? = { _ in return nil } + var getTransitionView: (String) -> (UIView, ASDisplayNode?, (Bool) -> Void)? = { _ in return nil } func animateIn(initiated: @escaping () -> Void, completion: @escaping () -> Void = {}) { let _ = (self.ready.get() @@ -502,7 +596,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI } for (identifier, itemNode) in strongSelf.itemNodes { - if let (transitionView, _) = strongSelf.getTransitionView(identifier) { + if let (transitionView, _, _) = strongSelf.getTransitionView(identifier) { itemNode.animateFrom(transitionView) } else { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) @@ -550,18 +644,22 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI } for (identifier, itemNode) in self.itemNodes { - if let (transitionView, completion) = self.getTransitionView(identifier) { - itemNode.animateTo(transitionView, completion: completion) + if let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) { + itemNode.animateTo(transitionView, dustNode: maybeDustNode, completion: completion) } else { itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false) } } - self.messageNodes?.first?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - self.messageNodes?.first?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + if let topNode = self.messageNodes?.first { + topNode.layer.animateAlpha(from: topNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) + topNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } - self.messageNodes?.last?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - self.messageNodes?.last?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + if let bottomNode = self.messageNodes?.last { + bottomNode.layer.animateAlpha(from: bottomNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) + bottomNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index 5dc2c5bc7e9..318c28c7c54 100644 --- a/submodules/MediaPlayer/BUILD +++ b/submodules/MediaPlayer/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/RingBuffer:RingBuffer", "//submodules/YuvConversion:YuvConversion", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift index 6d37744bb59..9df8abd8f4c 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift @@ -68,6 +68,8 @@ private func contextForCurrentThread() -> FFMpegMediaFrameSourceContext? { public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let queue: Queue private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? private let streamable: Bool @@ -98,9 +100,11 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - public init(queue: Queue, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { + public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { self.queue = queue self.postbox = postbox + self.userLocation = userLocation + self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath self.streamable = streamable @@ -181,13 +185,14 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let tempFilePath = self.tempFilePath let queue = self.queue let streamable = self.streamable + let userLocation = self.userLocation let video = self.video let preferSoftwareDecoding = self.preferSoftwareDecoding let fetchAutomatically = self.fetchAutomatically let maximumFetchSize = self.maximumFetchSize self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize) let (frames, endOfStream) = context.takeFrames(until: timestamp) @@ -228,6 +233,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let queue = self.queue let postbox = self.postbox + let userLocation = self.userLocation let resourceReference = self.resourceReference let tempFilePath = self.tempFilePath let streamable = self.streamable @@ -245,7 +251,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.performWithContext { [weak self] context in let _ = currentSemaphore.swap(context.currentSemaphore) - context.initializeState(postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize) context.seek(timestamp: timestamp, completed: { streamDescriptionsAndTimestamp in queue.async { diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index 289175dd5c9..3a255009f26 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -196,7 +196,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable, let statsCategory = context.statsCategory else { + guard let postbox = context.postbox, let resourceReference = context.resourceReference, let streamable = context.streamable, let userLocation = context.userLocation, let userContentType = context.userContentType, let statsCategory = context.statsCategory else { return 0 } @@ -250,12 +250,12 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe if streamable { if context.tempFilePath == nil { let fetchRange: Range = context.readingOffset ..< Int64.max - context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) } } else if !context.requestedCompleteFetch && context.fetchAutomatically { context.requestedCompleteFetch = true if context.tempFilePath == nil { - context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) } } } @@ -276,6 +276,8 @@ final class FFMpegMediaFrameSourceContext: NSObject { var closed = false fileprivate var postbox: Postbox? + fileprivate var userLocation: MediaResourceUserLocation? + fileprivate var userContentType: MediaResourceUserContentType? fileprivate var resourceReference: MediaResourceReference? fileprivate var tempFilePath: String? fileprivate var streamable: Bool? @@ -320,7 +322,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.keepDataDisposable.dispose() } - func initializeState(postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?) { + func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?) { if self.readingError || self.initializedState != nil { return } @@ -332,6 +334,8 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.tempFilePath = tempFilePath self.streamable = streamable self.statsCategory = video ? .video : .audio + self.userLocation = userLocation + self.userContentType = video ? .video : .audio self.preferSoftwareDecoding = preferSoftwareDecoding self.fetchAutomatically = fetchAutomatically self.maximumFetchSize = maximumFetchSize @@ -342,12 +346,12 @@ final class FFMpegMediaFrameSourceContext: NSObject { if streamable { if self.tempFilePath == nil { - self.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, range: (0 ..< Int64.max, .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } } else if !self.requestedCompleteFetch && self.fetchAutomatically { self.requestedCompleteFetch = true if self.tempFilePath == nil { - self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } } @@ -449,7 +453,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { if streamable { if self.tempFilePath == nil { - self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, range: (0 ..< Int64.max, .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) + self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } self.requestedCompleteFetch = true } diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index e1ab42f53fc..f33b11fbaf3 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -101,6 +101,8 @@ private final class MediaPlayerContext { private let audioSessionManager: ManagedAudioSession private let postbox: Postbox + private let userLocation: MediaResourceUserLocation + private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? private let streamable: MediaPlayerStreaming @@ -133,7 +135,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, ambient: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, ambient: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -141,6 +143,8 @@ private final class MediaPlayerContext { self.playerStatus = playerStatus self.audioLevelPipe = audioLevelPipe self.postbox = postbox + self.userLocation = userLocation + self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath self.streamable = streamable @@ -300,7 +304,7 @@ private final class MediaPlayerContext { let _ = self.playerStatusValue.swap(status) } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, streamable: self.streamable.enabled, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, userLocation: self.userLocation, userContentType: self.userContentType, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, streamable: self.streamable.enabled, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2) let disposable = MetaDisposable() let updatedSeekState: MediaPlayerSeekState? if let loadedDuration = loadedDuration { @@ -1061,10 +1065,10 @@ public final class MediaPlayer { } } - public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, ambient: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) { + public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, ambient: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) { let audioLevelPipe = self.audioLevelPipe self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, ambient: ambient, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, ambient: ambient, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift index 47730aaa06f..b67019af30e 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift @@ -25,9 +25,9 @@ private final class FramePreviewContext { } } -private func initializedPreviewContext(queue: Queue, postbox: Postbox, fileReference: FileMediaReference) -> Signal, NoError> { +private func initializedPreviewContext(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) -> Signal, NoError> { return Signal { subscriber in - let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, fileReference: fileReference) + let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference) let readyDisposable = (source.ready |> filter { $0 }).start(next: { _ in subscriber.putNext(QueueLocalObject(queue: queue, generate: { @@ -49,10 +49,10 @@ private final class MediaPlayerFramePreviewImpl { private var nextFrameTimestamp: Double? fileprivate let framePipe = ValuePipe() - init(queue: Queue, postbox: Postbox, fileReference: FileMediaReference) { + init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) { self.queue = queue self.context = Promise() - self.context.set(initializedPreviewContext(queue: queue, postbox: postbox, fileReference: fileReference)) + self.context.set(initializedPreviewContext(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference)) } deinit { @@ -131,11 +131,11 @@ public final class MediaPlayerFramePreview: FramePreview { } } - public init(postbox: Postbox, fileReference: FileMediaReference) { + public init(postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) { let queue = Queue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return MediaPlayerFramePreviewImpl(queue: queue, postbox: postbox, fileReference: fileReference) + return MediaPlayerFramePreviewImpl(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference) }) } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index 8448d70ddb6..0bcd1639732 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Display import SwiftSignalKit import RangeSet +import TextFormat public enum MediaPlayerScrubbingNodeCap { case square @@ -29,6 +30,39 @@ public struct MediaPlayerScrubbingChapter: Equatable { } } +public func parseMediaPlayerChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] { + var existingTimecodes = Set() + var timecodeRanges: [(NSRange, TelegramTimecode)] = [] + var lineRanges: [NSRange] = [] + string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in + if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode { + if !existingTimecodes.contains(timecode.time) { + timecodeRanges.append((range, timecode)) + existingTimecodes.insert(timecode.time) + } + } + }) + (string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in + lineRanges.append(range) + }) + + var chapters: [MediaPlayerScrubbingChapter] = [] + for (timecodeRange, timecode) in timecodeRanges { + inner: for lineRange in lineRanges { + if lineRange.contains(timecodeRange.location) { + if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 { + var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "") + title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters) + chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time)) + } + break inner + } + } + } + + return chapters +} + private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate { var beginScrubbing: (() -> Void)? var endScrubbing: ((Bool) -> Void)? diff --git a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift index bdd451b19bc..f9abf15ea01 100644 --- a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift +++ b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift @@ -5,14 +5,14 @@ import Postbox import TelegramCore import FFMpegBinding -public func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceReference, duration: Double) -> Signal { +public func preloadVideoResource(postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, duration: Double) -> Signal { return Signal { subscriber in let queue = Queue() let disposable = MetaDisposable() queue.async { let maximumFetchSize = 2 * 1024 * 1024 + 128 * 1024 //let maximumFetchSize = 128 - let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, resourceReference: resourceReference, tempFilePath: nil, streamable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) + let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, streamable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) let source = QueueLocalObject(queue: queue, generate: { return sourceImpl }) diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 12c3fc1bd92..741d9a3647c 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -27,6 +27,8 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa let fetchDisposable = MetaDisposable() let isInitialized = context.videoStream != nil || context.automaticallyFetchHeader let mediaBox = context.mediaBox + let userLocation = context.userLocation + let userContentType = context.userContentType let reference = context.fileReference.resourceReference(context.fileReference.media.resource) let disposable = data.start(next: { result in let (data, isComplete) = result @@ -35,7 +37,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa semaphore.signal() } else { if isInitialized { - fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, reference: reference, ranges: [(requestRange, .maximum)]).start()) + fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: reference, ranges: [(requestRange, .maximum)]).start()) } requiredDataIsNotLocallyAvailable?() } @@ -98,6 +100,8 @@ private final class SoftwareVideoStream { private final class UniversalSoftwareVideoSourceImpl { fileprivate let mediaBox: MediaBox + fileprivate let userLocation: MediaResourceUserLocation + fileprivate let userContentType: MediaResourceUserContentType fileprivate let fileReference: FileMediaReference fileprivate let size: Int64 fileprivate let automaticallyFetchHeader: Bool @@ -115,12 +119,14 @@ private final class UniversalSoftwareVideoSourceImpl { fileprivate var currentNumberOfReads: Int = 0 fileprivate var currentReadBytes: Int64 = 0 - init?(mediaBox: MediaBox, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool = false) { + init?(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool = false) { guard let size = fileReference.media.size else { return nil } self.mediaBox = mediaBox + self.userLocation = userLocation + self.userContentType = userContentType self.fileReference = fileReference self.size = size self.automaticallyFetchHeader = automaticallyFetchHeader @@ -289,6 +295,8 @@ private enum UniversalSoftwareVideoSourceState { private final class UniversalSoftwareVideoSourceThreadParams: NSObject { let mediaBox: MediaBox + let userLocation: MediaResourceUserLocation + let userContentType: MediaResourceUserContentType let fileReference: FileMediaReference let state: ValuePromise let cancelInitialization: Signal @@ -297,6 +305,8 @@ private final class UniversalSoftwareVideoSourceThreadParams: NSObject { init( mediaBox: MediaBox, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal, @@ -304,6 +314,8 @@ private final class UniversalSoftwareVideoSourceThreadParams: NSObject { hintVP9: Bool ) { self.mediaBox = mediaBox + self.userLocation = userLocation + self.userContentType = userContentType self.fileReference = fileReference self.state = state self.cancelInitialization = cancelInitialization @@ -333,7 +345,7 @@ private final class UniversalSoftwareVideoSourceThread: NSObject { let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.none), userInfo: nil, repeats: false) runLoop.add(timer, forMode: .common) - let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, fileReference: params.fileReference, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) + let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, userLocation: params.userLocation, userContentType: params.userContentType, fileReference: params.fileReference, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) Thread.current.threadDictionary["source"] = source while true { @@ -391,8 +403,8 @@ public final class UniversalSoftwareVideoSource { } } - public init(mediaBox: MediaBox, fileReference: FileMediaReference, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { - self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, fileReference: fileReference, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) + public init(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { + self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) self.thread.name = "UniversalSoftwareVideoSource" self.thread.start() } diff --git a/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift b/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift index fa1a4287bea..60e40d690f9 100644 --- a/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift +++ b/submodules/PaymentMethodUI/Sources/AddPaymentMethodSheetScreen.swift @@ -196,7 +196,7 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent { }) } )), - backgroundColor: .white, + backgroundColor: .color(.white), animateOut: animateOut ), environment: { @@ -204,6 +204,8 @@ private final class AddPaymentMethodSheetComponent: CombinedComponent { SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: false, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: nil, dismiss: { animated in if animated { animateOut.invoke(Action { _ in diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 22230f7200d..722045ef8b2 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -32,11 +32,30 @@ public func peerInfoProfilePhotos(context: AccountContext, peerId: EnginePeer.Id |> distinctUntilChanged |> mapToSignal { entries -> Signal<(Bool, [AvatarGalleryEntry])?, NoError> in if let entries = entries { - if let firstEntry = entries.first { - return context.account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer -> Signal<(Bool, [AvatarGalleryEntry])?, NoError>in - return fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: peer, firstEntry: firstEntry) - |> map(Optional.init) + if var firstEntry = entries.first { + return context.account.postbox.peerView(id: peerId) + |> mapToSignal { peerView -> Signal<(Bool, [AvatarGalleryEntry])?, NoError>in + if let peer = peerViewMainPeer(peerView) { + var secondEntry: TelegramMediaImage? + var lastEntry: TelegramMediaImage? + if let cachedData = peerView.cachedData as? CachedUserData { + if let firstRepresentation = firstEntry.representations.first, firstRepresentation.representation.isPersonal { + if firstRepresentation.representation.hasVideo, case let .known(photo) = cachedData.personalPhoto, let peerReference = PeerReference(peer) { + firstEntry = .topImage(firstEntry.representations, photo?.videoRepresentations.map { VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) } ?? [], firstEntry.peer, firstEntry.indexData, firstEntry.immediateThumbnailData, nil) + } + if case let .known(photo) = cachedData.photo { + secondEntry = photo + } + } + if case let .known(photo) = cachedData.fallbackPhoto { + lastEntry = photo + } + } + return fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: peer, firstEntry: firstEntry, secondEntry: secondEntry, lastEntry: lastEntry) + |> map(Optional.init) + } else { + return .single(nil) + } } } else { return .single((true, [])) @@ -66,7 +85,7 @@ public func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: Peer public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, GalleryItemIndexData?, Data?, String?) - case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, Int32?, GalleryItemIndexData?, MessageId?, Data?, String?) + case image(MediaId, TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Peer?, Int32?, GalleryItemIndexData?, MessageId?, Data?, String?, Bool) public init(representation: TelegramMediaImageRepresentation, peer: Peer) { self = .topImage([ImageRepresentationWithReference(representation: representation, reference: MediaResourceReference.standalone(resource: representation.resource))], [], peer, nil, nil, nil) @@ -79,7 +98,7 @@ public enum AvatarGalleryEntry: Equatable { return .resource(last.representation.resource.id.stringRepresentation) } return .topImage - case let .image(id, _, representations, _, _, _, _, _, _, _): + case let .image(id, _, representations, _, _, _, _, _, _, _, _): if let last = representations.last { return .resource(last.representation.resource.id.stringRepresentation) } @@ -91,7 +110,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, _, peer, _, _, _): return peer - case let .image(_, _, _, _, peer, _, _, _, _, _): + case let .image(_, _, _, _, peer, _, _, _, _, _, _): return peer } } @@ -100,7 +119,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(representations, _, _, _, _, _): return representations - case let .image(_, _, representations, _, _, _, _, _, _, _): + case let .image(_, _, representations, _, _, _, _, _, _, _, _): return representations } } @@ -109,7 +128,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, _, _, _, immediateThumbnailData, _): return immediateThumbnailData - case let .image(_, _, _, _, _, _, _, _, immediateThumbnailData, _): + case let .image(_, _, _, _, _, _, _, _, immediateThumbnailData, _, _): return immediateThumbnailData } } @@ -118,7 +137,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, videoRepresentations, _, _, _, _): return videoRepresentations - case let .image(_, _, _, videoRepresentations, _, _, _, _, _, _): + case let .image(_, _, _, videoRepresentations, _, _, _, _, _, _, _): return videoRepresentations } } @@ -127,7 +146,7 @@ public enum AvatarGalleryEntry: Equatable { switch self { case let .topImage(_, _, _, indexData, _, _): return indexData - case let .image(_, _, _, _, _, _, indexData, _, _, _): + case let .image(_, _, _, _, _, _, indexData, _, _, _, _): return indexData } } @@ -140,8 +159,8 @@ public enum AvatarGalleryEntry: Equatable { } else { return false } - case let .image(lhsId, lhsImageReference, lhsRepresentations, lhsVideoRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId, lhsImmediateThumbnailData, lhsCategory): - if case let .image(rhsId, rhsImageReference, rhsRepresentations, rhsVideoRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId, rhsImmediateThumbnailData, rhsCategory) = rhs, lhsId == rhsId, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, lhsVideoRepresentations == rhsVideoRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId, lhsImmediateThumbnailData == rhsImmediateThumbnailData, lhsCategory == rhsCategory { + case let .image(lhsId, lhsImageReference, lhsRepresentations, lhsVideoRepresentations, lhsPeer, lhsDate, lhsIndexData, lhsMessageId, lhsImmediateThumbnailData, lhsCategory, lhsIsFallback): + if case let .image(rhsId, rhsImageReference, rhsRepresentations, rhsVideoRepresentations, rhsPeer, rhsDate, rhsIndexData, rhsMessageId, rhsImmediateThumbnailData, rhsCategory, rhsIsFallback) = rhs, lhsId == rhsId, lhsImageReference == rhsImageReference, lhsRepresentations == rhsRepresentations, lhsVideoRepresentations == rhsVideoRepresentations, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsIndexData == rhsIndexData, lhsMessageId == rhsMessageId, lhsImmediateThumbnailData == rhsImmediateThumbnailData, lhsCategory == rhsCategory, lhsIsFallback == rhsIsFallback { return true } else { return false @@ -168,8 +187,8 @@ public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryE let indexData = GalleryItemIndexData(position: index, totalCount: count) if case let .topImage(representations, videoRepresentations, peer, _, immediateThumbnailData, category) = entry { updatedEntries.append(.topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category)) - } else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category) = entry { - updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category)) + } else if case let .image(id, reference, representations, videoRepresentations, peer, date, _, messageId, immediateThumbnailData, category, isFallback) = entry { + updatedEntries.append(.image(id, reference, representations, videoRepresentations, peer, date, indexData, messageId, immediateThumbnailData, category, isFallback)) } index += 1 } @@ -195,7 +214,7 @@ public func initialAvatarGalleryEntries(account: Account, engine: TelegramEngine if photo.immediateThumbnailData == nil, let firstEntry = initialEntries.first, let firstRepresentation = firstEntry.representations.first { representations.insert(firstRepresentation, at: 0) } - return [.image(photo.imageId, photo.reference, representations, photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil)] + return [.image(photo.imageId, photo.reference, representations, photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, nil, nil, nil, photo.immediateThumbnailData, nil, false)] } else { if case .known = peerPhoto { return [] @@ -227,7 +246,7 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { var initialMediaIds = Set() for entry in initialEntries { - if case let .image(mediaId, _, _, _, _, _, _, _, _, _) = entry { + if case let .image(mediaId, _, _, _, _, _, _, _, _, _, _) = entry { initialMediaIds.insert(mediaId) } } @@ -239,24 +258,24 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account photosCount += 1 for entry in initialEntries { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _) = entry { - result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil)) + if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _, _) = entry { + result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil, false)) index += 1 } } } let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) index += 1 } } else { for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.imageId, photo.image.reference, first.representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) } else { - result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) } index += 1 } @@ -268,7 +287,7 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account } } -public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: Peer, firstEntry: AvatarGalleryEntry) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { +public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account, peer: Peer, firstEntry: AvatarGalleryEntry, secondEntry: TelegramMediaImage?, lastEntry: TelegramMediaImage?) -> Signal<(Bool, [AvatarGalleryEntry]), NoError> { let initialEntries = [firstEntry] return Signal<(Bool, [AvatarGalleryEntry]), NoError>.single((false, initialEntries)) |> then( @@ -284,7 +303,7 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { var initialMediaIds = Set() for entry in initialEntries { - if case let .image(mediaId, _, _, _, _, _, _, _, _, _) = entry { + if case let .image(mediaId, _, _, _, _, _, _, _, _, _, _) = entry { initialMediaIds.insert(mediaId) } } @@ -298,8 +317,8 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account photosCount += 1 for entry in initialEntries { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _) = entry { - result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil)) + if case let .image(mediaId, imageReference, representations, videoRepresentations, peer, _, _, _, thumbnailData, _, _) = entry { + result.append(.image(mediaId, imageReference, representations, videoRepresentations, peer, nil, indexData, nil, thumbnailData, nil, false)) index += 1 } } @@ -309,16 +328,28 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account } let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photosCount)) - result.append(.image(photo.image.imageId, photo.image.reference, representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) index += 1 } } else { + var photos = photos + if let secondEntry { + photos.insert(TelegramPeerPhoto(image: secondEntry, reference: secondEntry.reference, date: photos.first?.date ?? 0, index: 1, totalCount: 0, messageId: nil), at: 1) + } + if let lastEntry { + photos.append(TelegramPeerPhoto(image: lastEntry, reference: lastEntry.reference, date: 0, index: photos.count, totalCount: 0, messageId: nil)) + } for photo in photos { let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.imageId, photo.image.reference, first.representations, photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + var videoRepresentations: [VideoRepresentationWithReference] = first.videoRepresentations + let isPersonal = first.representations.first?.representation.isPersonal == true + if videoRepresentations.isEmpty, !isPersonal { + videoRepresentations = photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }) + } + result.append(.image(photo.image.imageId, photo.image.reference, first.representations, videoRepresentations, peer, secondEntry != nil ? 0 : photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, false)) } else { - result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil)) + result.append(.image(photo.image.imageId, photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), photo.image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId, photo.image.immediateThumbnailData, nil, photo.image.id == lastEntry?.id)) } index += 1 } @@ -343,6 +374,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let context: AccountContext private let peer: Peer private let sourceCorners: SourceCorners + private let isSuggested: Bool private var presentationData: PresentationData @@ -382,10 +414,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr private let editDisposable = MetaDisposable () - public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise?) -> Void, synchronousLoad: Bool = false) { + public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, isSuggested: Bool = false, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise?) -> Void, synchronousLoad: Bool = false) { self.context = context self.peer = peer self.sourceCorners = sourceCorners + self.isSuggested = isSuggested self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.replaceRootController = replaceRootController @@ -429,8 +462,8 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr let isFirstTime = strongSelf.entries.isEmpty var entries = entries - if !isFirstTime, let updated = entries.first, case let .image(mediaId, imageReference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.entries.first, case let .topImage(representations, _, _, _, _, _) = previous { - let firstEntry = AvatarGalleryEntry.image(mediaId, imageReference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption) + if !isFirstTime, let updated = entries.first, case let .image(mediaId, imageReference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, _) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.entries.first, case let .topImage(representations, _, _, _, _, _) = previous { + let firstEntry = AvatarGalleryEntry.image(mediaId, imageReference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, false) entries.remove(at: 0) entries.insert(firstEntry, at: 0) } @@ -439,6 +472,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if strongSelf.centralEntryIndex == nil { strongSelf.centralEntryIndex = 0 } + + if strongSelf.isSuggested, let firstEntry = entries.first { + strongSelf.navigationItem.title = !firstEntry.videoRepresentations.isEmpty ? strongSelf.presentationData.strings.Conversation_SuggestedVideoTitle : strongSelf.presentationData.strings.Conversation_SuggestedPhotoTitle + } + if strongSelf.isViewLoaded { strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? { self?.deleteEntry(entry) @@ -746,13 +784,13 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr if self.peer.id == self.context.account.peerId { } else { } - case let .image(_, reference, _, _, _, _, _, _, _, _): + case let .image(_, reference, _, _, _, _, _, _, _, _, _): if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer) { if let reference = reference { let _ = (self.context.engine.accountData.updatePeerPhotoExisting(reference: reference) |> deliverOnMainQueue).start(next: { [weak self] photo in - if let strongSelf = self, let photo = photo, let firstEntry = strongSelf.entries.first, case let .image(_, _, _, _, _, index, indexData, messageId, _, caption) = firstEntry { - let updatedEntry = AvatarGalleryEntry.image(photo.imageId, photo.reference, photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), strongSelf.peer, index, indexData, messageId, photo.immediateThumbnailData, caption) + if let strongSelf = self, let photo = photo, let firstEntry = strongSelf.entries.first, case let .image(_, _, _, _, _, index, indexData, messageId, _, caption, _) = firstEntry { + let updatedEntry = AvatarGalleryEntry.image(photo.imageId, photo.reference, photo.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), photo.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatarList(peer: peerReference, resource: $0.resource)) }), strongSelf.peer, index, indexData, messageId, photo.immediateThumbnailData, caption, false) for (lhs, rhs) in zip(firstEntry.representations, updatedEntry.representations) { if lhs.representation.dimensions == rhs.representation.dimensions { @@ -809,8 +847,13 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr self?.dismissImmediately() }) })) + + var isFallback = false + if case let .image(_, _, _, _, _, _, _, _, _, _, isFallbackValue) = rawEntry { + isFallback = isFallbackValue + } - if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 { + if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback { let title: String if let _ = rawEntry.videoRepresentations.last { title = self.presentationData.strings.ProfilePhoto_SetMainVideo @@ -870,11 +913,14 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } } } - case let .image(_, reference, _, _, _, _, _, messageId, _, _): + case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback): if self.peer.id == self.context.account.peerId { - if let reference = reference { + if isFallback { + let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start() + } else if let reference = reference { let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start() } + if entry == self.entries.first { dismiss = true } else { diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 2f36df53a53..6278a700e11 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -107,10 +107,14 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { var buttonText: String? var canShare = true switch entry { - case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _): - nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" - if let date = date { + case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback): + if date != 0 || isFallback { + nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + } + if let date = date, date != 0 { dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string + } else if isFallback { + dateText = !videoRepresentations.isEmpty ? self.strings.ProfilePhoto_PublicVideo : self.strings.ProfilePhoto_PublicPhoto } if (!videoRepresentations.isEmpty) { diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 3554ab1dab7..536e0d421ea 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -106,7 +106,7 @@ class PeerAvatarImageGalleryItem: GalleryItem { switch self.entry { case let .topImage(representations, _, _, _, _, _): content = representations - case let .image(_, _, representations, _, _, _, _, _, _, _): + case let .image(_, _, representations, _, _, _, _, _, _, _, _): content = representations } @@ -195,10 +195,16 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { subject = .image(entry.representations) actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved } - let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll) - shareController.actionCompleted = { [weak self] in - if let strongSelf = self, let actionCompletionText = actionCompletionText { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + var forceTheme: PresentationTheme? + if !presentationData.theme.overallDarkAppearance { + forceTheme = defaultDarkColorPresentationTheme + } + + let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme) + shareController.actionCompleted = { + if let actionCompletionText = actionCompletionText { interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil) } } @@ -257,12 +263,12 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.zoomableContent = (largestSize.dimensions.cgSize, self.contentNode) if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: representations[largestIndex].reference).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start()) } var id: Int64 var category: String? - if case let .image(mediaId, _, _, _, _, _, _, _, _, categoryValue) = entry { + if case let .image(mediaId, _, _, _, _, _, _, _, _, categoryValue, _) = entry { id = mediaId.id category = categoryValue } else { @@ -275,7 +281,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if video != previousVideoRepresentations?.last { let mediaManager = self.context.sharedContext.mediaManager let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, category), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) + let videoContent = NativeVideoContent(id: .profileVideo(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay) videoNode.isUserInteractionEnabled = false videoNode.isHidden = true @@ -602,12 +608,12 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { switch entry { case let .topImage(topRepresentations, _, _, _, _, _): representations = topRepresentations - case let .image(_, _, imageRepresentations, _, _, _, _, _, _, _): + case let .image(_, _, imageRepresentations, _, _, _, _, _, _, _, _): representations = imageRepresentations } if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: representations[largestIndex].reference).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start()) } default: break diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 245d4eee81f..a8c7378c054 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -19,6 +19,7 @@ import GalleryUI import UniversalMediaPlayer import RadialStatusNode import TelegramUIPreferences +import AvatarNode private class PeerInfoAvatarListLoadingStripNode: ASImageNode { private var currentInHierarchy = false @@ -94,7 +95,7 @@ private struct CustomListItemResourceId { public enum PeerInfoAvatarListItem: Equatable { case custom(ASDisplayNode) case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) - case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?) + case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?, Bool) var id: MediaResourceId { switch self { @@ -103,7 +104,7 @@ public enum PeerInfoAvatarListItem: Equatable { case let .topImage(representations, _, _): let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation return representation.resource.id - case let .image(_, representations, _, _): + case let .image(_, representations, _, _, _): let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation return representation.resource.id } @@ -118,7 +119,7 @@ public enum PeerInfoAvatarListItem: Equatable { } else { return false } - } else if case let .image(_, rhsRepresentations, _, _) = self { + } else if case let .image(_, rhsRepresentations, _, _, _) = self { if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }), let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) { return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation) @@ -128,7 +129,7 @@ public enum PeerInfoAvatarListItem: Equatable { } else { return false } - } else if case let .image(_, lhsRepresentations, _, _) = self { + } else if case let .image(_, lhsRepresentations, _, _, _) = self { if case let .topImage(rhsRepresentations, _, _) = self { if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }), let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) { @@ -136,7 +137,7 @@ public enum PeerInfoAvatarListItem: Equatable { } else { return false } - } else if case let .image(_, rhsRepresentations, _, _) = self { + } else if case let .image(_, rhsRepresentations, _, _, _) = self { if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }), let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) { return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation) @@ -157,7 +158,7 @@ public enum PeerInfoAvatarListItem: Equatable { return [] case let .topImage(representations, _, _): return representations - case let .image(_, representations, _, _): + case let .image(_, representations, _, _, _): return representations } } @@ -168,20 +169,29 @@ public enum PeerInfoAvatarListItem: Equatable { return [] case let .topImage(_, videoRepresentations, _): return videoRepresentations - case let .image(_, _, videoRepresentations, _): + case let .image(_, _, videoRepresentations, _, _): return videoRepresentations } } + var isFallback: Bool { + switch self { + case .custom, .topImage: + return false + case let .image(_, _, _, _, isFallback): + return isFallback + } + } + public init?(entry: AvatarGalleryEntry) { switch entry { case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): self = .topImage(representations, videoRepresentations, immediateThumbnailData) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback): if representations.isEmpty { return nil } - self = .image(reference, representations, videoRepresentations, immediateThumbnailData) + self = .image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback) } } } @@ -241,7 +251,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } if let videoContent = self.videoContent { let duration: Double = (self.videoStartTimestamp ?? 0.0) + 4.0 - self.preloadDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, resourceReference: videoContent.fileReference.resourceReference(videoContent.fileReference.media.resource), duration: duration).start()) + self.preloadDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: videoContent.fileReference.resourceReference(videoContent.fileReference.media.resource), duration: duration).start()) } } } @@ -444,7 +454,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail @@ -458,7 +468,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) + let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) if videoContent.id != self.videoContent?.id { self.videoContent = videoContent @@ -511,6 +521,7 @@ private let fadeWidth: CGFloat = 70.0 public final class PeerInfoAvatarListContainerNode: ASDisplayNode { private let context: AccountContext + private let isSettings: Bool public var peer: Peer? public let controlsContainerNode: ASDisplayNode @@ -525,6 +536,10 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { var highlightedSide: Bool? public let stripContainerNode: ASDisplayNode public let highlightContainerNode: ASDisplayNode + public let setByYouNode: ImmediateTextNode + private let setByYouImageNode: ImageNode + private var setByYouTapRecognizer: UITapGestureRecognizer? + public private(set) var galleryEntries: [AvatarGalleryEntry] = [] private var items: [PeerInfoAvatarListItem] = [] private var itemNodes: [MediaResourceId: PeerInfoAvatarListItemNode] = [:] @@ -643,8 +658,9 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.playerUpdateTimer = nil } - public init(context: AccountContext) { + public init(context: AccountContext, isSettings: Bool = false) { self.context = context + self.isSettings = isSettings self.contentNode = ASDisplayNode() @@ -698,6 +714,14 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.highlightContainerNode.addSubnode(self.leftHighlightNode) self.highlightContainerNode.addSubnode(self.rightHighlightNode) + self.setByYouNode = ImmediateTextNode() + self.setByYouNode.alpha = 0.0 + self.setByYouNode.isUserInteractionEnabled = false + + self.setByYouImageNode = ImageNode() + self.setByYouImageNode.alpha = 0.0 + self.setByYouImageNode.isUserInteractionEnabled = false + self.controlsContainerNode = ASDisplayNode() self.controlsContainerNode.isUserInteractionEnabled = false @@ -767,6 +791,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.controlsContainerNode.addSubnode(self.stripContainerNode) self.controlsClippingNode.addSubnode(self.controlsContainerNode) self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode) + self.stripContainerNode.addSubnode(self.setByYouNode) + self.stripContainerNode.addSubnode(self.setByYouImageNode) self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in guard let strongSelf = self else { @@ -866,6 +892,19 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.positionDisposable.dispose() } + public override func didLoad() { + super.didLoad() + + let setByYouTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.setByYouTapped)) + self.setByYouNode.isUserInteractionEnabled = true + self.setByYouNode.view.addGestureRecognizer(setByYouTapRecognizer) + self.setByYouTapRecognizer = setByYouTapRecognizer + } + + @objc private func setByYouTapped() { + self.selectLastItem() + } + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return super.hitTest(point, with: event) } @@ -881,6 +920,17 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } } + public func selectLastItem() { + let previousIndex = self.currentIndex + self.currentIndex = self.items.count - 1 + if self.currentIndex != previousIndex { + self.currentIndexUpdated?() + } + if let size = self.validLayout { + self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true) + } + } + public func updateEntryIsHidden(entry: AvatarGalleryEntry?) { if let entry = entry, let index = self.galleryEntries.firstIndex(of: entry) { self.currentItemNode?.isHidden = index == self.currentIndex @@ -992,7 +1042,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } func setMainItem(_ item: PeerInfoAvatarListItem) { - guard case let .image(imageReference, _, _, _) = item else { + guard case let .image(imageReference, _, _, _, _) = item else { return } var items: [PeerInfoAvatarListItem] = [] @@ -1002,16 +1052,16 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): entries.append(entry) items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback): if representations.isEmpty { continue } if imageReference == reference { entries.insert(entry, at: 0) - items.insert(.image(reference, representations, videoRepresentations, immediateThumbnailData), at: 0) + items.insert(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback), at: 0) } else { entries.append(entry) - items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) + items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback)) } } } @@ -1030,7 +1080,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } public func deleteItem(_ item: PeerInfoAvatarListItem) -> Bool { - guard case let .image(imageReference, _, _, _) = item else { + guard case let .image(imageReference, _, _, _, _) = item else { return false } @@ -1045,13 +1095,13 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _): entries.append(entry) items.append(.topImage(representations, videoRepresentations, immediateThumbnailData)) - case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _): + case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback): if representations.isEmpty { continue } if imageReference != reference { entries.append(entry) - items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData)) + items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback)) } else { deletedIndex = index } @@ -1117,8 +1167,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } var synchronous = false - if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(mediaId, reference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(representations, _, _, _, _, _) = previous { - let firstEntry = AvatarGalleryEntry.image(mediaId, reference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption) + if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(mediaId, reference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, _) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(representations, _, _, _, _, _) = previous { + let firstEntry = AvatarGalleryEntry.image(mediaId, reference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, false) entries.remove(at: 0) entries.insert(firstEntry, at: 0) synchronous = true @@ -1296,6 +1346,46 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { } } } + + if !self.items.isEmpty, self.currentIndex >= 0 && self.currentIndex < self.items.count { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let currentItem = self.items[self.currentIndex] + + var photoTitle: String? + var hasLink = false + var fallbackImageSignal: Signal? + if let representation = currentItem.representations.first?.representation, representation.isPersonal { + photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_CustomVideo : presentationData.strings.UserInfo_CustomPhoto + } else if currentItem.isFallback, let representation = currentItem.representations.first?.representation, self.isSettings { + photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_PublicVideo : presentationData.strings.UserInfo_PublicPhoto + } else if self.currentIndex == 0, let lastItem = self.items.last, lastItem.isFallback, let representation = lastItem.representations.first?.representation, self.isSettings { + photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_PublicVideo : presentationData.strings.UserInfo_PublicPhoto + hasLink = true + if let peer = self.peer { + fallbackImageSignal = peerAvatarCompleteImage(account: self.context.account, peer: EnginePeer(peer), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)) + } + } + + if let photoTitle = photoTitle { + transition.updateAlpha(node: self.setByYouNode, alpha: 0.7) + self.setByYouNode.attributedText = NSAttributedString(string: photoTitle, font: Font.regular(12.0), textColor: UIColor.white) + let setByYouSize = self.setByYouNode.updateLayout(size) + self.setByYouNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - setByYouSize.width) / 2.0), y: 17.0), size: setByYouSize) + self.setByYouNode.isUserInteractionEnabled = hasLink + } else { + transition.updateAlpha(node: self.setByYouNode, alpha: 0.0) + self.setByYouNode.isUserInteractionEnabled = false + } + + if let fallbackImageSignal = fallbackImageSignal { + self.setByYouImageNode.setSignal(fallbackImageSignal) + transition.updateAlpha(node: self.setByYouImageNode, alpha: 1.0) + self.setByYouImageNode.frame = CGRect(origin: CGPoint(x: self.setByYouNode.frame.minX - 32.0, y: 11.0), size: CGSize(width: 28.0, height: 28.0)) + } else { + transition.updateAlpha(node: self.setByYouImageNode, alpha: 0.0) + } + } + for itemNode in addedItemNodesForAdditiveTransition { transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: additiveTransitionOffset, y: 0.0)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index a160b2537d4..4a41f97e2ae 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -14,6 +14,7 @@ import PresentationDataUtils import ItemListPeerItem import ItemListPeerActionItem import InviteLinksUI +import UndoUI private final class ChannelMembersControllerArguments { let context: AccountContext @@ -23,18 +24,23 @@ private final class ChannelMembersControllerArguments { let removePeer: (PeerId) -> Void let openPeer: (Peer) -> Void let inviteViaLink: () -> Void + let updateHideMembers: (Bool) -> Void + let displayHideMembersTip: (HideMembersDisabledReason) -> Void - init(context: AccountContext, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void, inviteViaLink: @escaping () -> Void) { + init(context: AccountContext, addMember: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void, inviteViaLink: @escaping () -> Void, updateHideMembers: @escaping (Bool) -> Void, displayHideMembersTip: @escaping (HideMembersDisabledReason) -> Void) { self.context = context self.addMember = addMember self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.openPeer = openPeer self.inviteViaLink = inviteViaLink + self.updateHideMembers = updateHideMembers + self.displayHideMembersTip = displayHideMembersTip } } private enum ChannelMembersSection: Int32 { + case hideMembers case addMembers case contacts case peers @@ -45,7 +51,14 @@ private enum ChannelMembersEntryStableId: Hashable { case peer(PeerId) } +private enum HideMembersDisabledReason: Equatable { + case notEnoughMembers(Int) + case notAllowed +} + private enum ChannelMembersEntry: ItemListNodeEntry { + case hideMembers(text: String, disabledReason: HideMembersDisabledReason?, isInteractive: Bool, value: Bool) + case hideMembersInfo(String) case addMember(PresentationTheme, String) case addMemberInfo(PresentationTheme, String) case inviteLink(PresentationTheme, String) @@ -55,6 +68,8 @@ private enum ChannelMembersEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { + case .hideMembers, .hideMembersInfo: + return ChannelMembersSection.hideMembers.rawValue case .addMember, .addMemberInfo, .inviteLink: return ChannelMembersSection.addMembers.rawValue case .contactsTitle: @@ -68,23 +83,39 @@ private enum ChannelMembersEntry: ItemListNodeEntry { var stableId: ChannelMembersEntryStableId { switch self { - case .addMember: - return .index(0) - case .addMemberInfo: - return .index(1) - case .inviteLink: - return .index(2) - case .contactsTitle: - return .index(3) - case .peersTitle: - return .index(4) - case let .peerItem(_, _, _, _, _, participant, _, _, _): - return .peer(participant.peer.id) + case .hideMembers: + return .index(0) + case .hideMembersInfo: + return .index(1) + case .addMember: + return .index(2) + case .addMemberInfo: + return .index(3) + case .inviteLink: + return .index(4) + case .contactsTitle: + return .index(5) + case .peersTitle: + return .index(6) + case let .peerItem(_, _, _, _, _, participant, _, _, _): + return .peer(participant.peer.id) } } static func ==(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { switch lhs { + case let .hideMembers(text, enabled, isInteractive, value): + if case .hideMembers(text, enabled, isInteractive, value) = rhs { + return true + } else { + return false + } + case let .hideMembersInfo(text): + if case .hideMembersInfo(text) = rhs { + return true + } else { + return false + } case let .addMember(lhsTheme, lhsText): if case let .addMember(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -153,32 +184,51 @@ private enum ChannelMembersEntry: ItemListNodeEntry { static func <(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { switch lhs { + case .hideMembers: + switch rhs { + case .hideMembers: + return false + default: + return true + } + case .hideMembersInfo: + switch rhs { + case .hideMembers, .hideMembersInfo: + return false + default: + return true + } case .addMember: - return true + switch rhs { + case .hideMembers, .hideMembersInfo, .addMember: + return false + default: + return true + } case .inviteLink: switch rhs { - case .addMember: + case .hideMembers, .hideMembersInfo, .addMember: return false default: return true } case .addMemberInfo: switch rhs { - case .addMember, .inviteLink: + case .hideMembers, .hideMembersInfo, .addMember, .inviteLink: return false default: return true } case .contactsTitle: switch rhs { - case .addMember, .addMemberInfo, .inviteLink: + case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink: return false default: return true } case .peersTitle: switch rhs { - case .addMember, .addMemberInfo, .inviteLink, .contactsTitle: + case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink, .contactsTitle: return false case let .peerItem(_, _, _, _, _, _, _, _, isContact): return !isContact @@ -193,7 +243,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry { return lhsIsContact case let .peerItem(rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex < rhsIndex - case .addMember, .addMemberInfo, .inviteLink: + case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink: return false } } @@ -202,6 +252,20 @@ private enum ChannelMembersEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelMembersControllerArguments switch self { + case let .hideMembers(text, disabledReason, isInteractive, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !value && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in + if let disabledReason { + arguments.displayHideMembersTip(disabledReason) + } else { + arguments.updateHideMembers(value) + } + }, activatedWhileDisabled: { + if let disabledReason { + arguments.displayHideMembersTip(disabledReason) + } + }) + case let .hideMembersInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .addMember(theme, text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { arguments.addMember() @@ -292,6 +356,50 @@ private func channelMembersControllerEntries(context: AccountContext, presentati var entries: [ChannelMembersEntry] = [] + var displayHideMembers = false + var canSetupHideMembers = false + if let channel = view.peers[view.peerId] as? TelegramChannel, case .group = channel.info { + displayHideMembers = true + canSetupHideMembers = channel.hasPermission(.banMembers) + } + + var membersHidden = false + var memberCount: Int? + if let cachedData = view.cachedData as? CachedChannelData, case let .known(value) = cachedData.membersHidden { + membersHidden = value.value + memberCount = cachedData.participantsSummary.memberCount.flatMap(Int.init) + } + + if displayHideMembers { + let appConfiguration = context.currentAppConfiguration.with({ $0 }) + var minMembers = 100 + if let data = appConfiguration.data, let value = data["hidden_members_group_size_min"] as? Double { + minMembers = Int(value) + } + + var disabledReason: HideMembersDisabledReason? + if memberCount ?? 0 < minMembers { + disabledReason = .notEnoughMembers(minMembers) + } else if !canSetupHideMembers { + disabledReason = .notAllowed + } + + var isInteractive = canSetupHideMembers + if canSetupHideMembers && !membersHidden && disabledReason != nil { + isInteractive = false + } + + entries.append(.hideMembers(text: presentationData.strings.GroupMembers_HideMembers, disabledReason: disabledReason, isInteractive: isInteractive, value: membersHidden)) + + let infoText: String + if membersHidden { + infoText = presentationData.strings.GroupMembers_MembersHiddenOn + } else { + infoText = presentationData.strings.GroupMembers_MembersHiddenOff + } + entries.append(.hideMembersInfo(infoText)) + } + if let participants = participants, let contacts = contacts { var canAddMember: Bool = false if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -388,6 +496,8 @@ public func channelMembersController(context: AccountContext, updatedPresentatio var getControllerImpl: (() -> ViewController?)? + var displayHideMembersTip: ((HideMembersDisabledReason) -> Void)? + let actionsDisposable = DisposableSet() let addMembersDisposable = MetaDisposable() @@ -516,6 +626,10 @@ public func channelMembersController(context: AccountContext, updatedPresentatio dismissInputImpl?() presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, parentNavigationController: controller.navigationController as? NavigationController), nil) } + }, updateHideMembers: { value in + let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start() + }, displayHideMembersTip: { disabledReason in + displayHideMembersTip?(disabledReason) }) let peerView = context.account.viewTracker.peerView(peerId) @@ -612,7 +726,18 @@ public func channelMembersController(context: AccountContext, updatedPresentatio } } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isGroup ? presentationData.strings.Group_Members_Title : presentationData.strings.Channel_Subscribers_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + var title: String = isGroup ? presentationData.strings.Group_Members_Title : presentationData.strings.Channel_Subscribers_Title + if let cachedData = view.cachedData as? CachedGroupData { + if let count = cachedData.participants?.participants.count { + title = presentationData.strings.GroupInfo_TitleMembers(Int32(count)) + } + } else if let cachedData = view.cachedData as? CachedChannelData { + if let count = cachedData.participantsSummary.memberCount { + title = presentationData.strings.GroupInfo_TitleMembers(count) + } + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, contacts: contacts, participants: peers, isGroup: isGroup), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: animateChanges) return (controllerState, (listState, arguments)) @@ -635,6 +760,22 @@ public func channelMembersController(context: AccountContext, updatedPresentatio dismissInputImpl = { [weak controller] in controller?.view.endEditing(true) } + displayHideMembersTip = { [weak controller] reason in + guard let controller else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let text: String + switch reason { + case let .notEnoughMembers(minCount): + text = presentationData.strings.PeerInfo_HideMembersLimitedParticipantCountText(Int32(minCount)) + case .notAllowed: + text = presentationData.strings.PeerInfo_HideMembersLimitedRights + } + controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_topics", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } getControllerImpl = { [weak controller] in return controller } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index c578d0c3583..f6f670583e5 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore @@ -21,6 +22,8 @@ import ItemListAddressItem import LocalizedPeerData import PhoneNumberFormat import UndoUI +import GalleryUI +import PeerAvatarGalleryUI private enum DeviceContactInfoAction { case sendMessage @@ -45,8 +48,9 @@ private final class DeviceContactInfoControllerArguments { let openAddress: (DeviceContactAddressData) -> Void let displayCopyContextMenu: (DeviceContactInfoEntryTag, String) -> Void let updateShareViaException: (Bool) -> Void + let openAvatar: (Peer) -> Void - init(context: AccountContext, isPlain: Bool, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void, displayCopyContextMenu: @escaping (DeviceContactInfoEntryTag, String) -> Void, updateShareViaException: @escaping (Bool) -> Void) { + init(context: AccountContext, isPlain: Bool, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void, displayCopyContextMenu: @escaping (DeviceContactInfoEntryTag, String) -> Void, updateShareViaException: @escaping (Bool) -> Void, openAvatar: @escaping (Peer) -> Void) { self.context = context self.isPlain = isPlain self.updateEditingName = updateEditingName @@ -62,6 +66,7 @@ private final class DeviceContactInfoControllerArguments { self.openAddress = openAddress self.displayCopyContextMenu = displayCopyContextMenu self.updateShareViaException = updateShareViaException + self.openAvatar = openAvatar } } @@ -122,7 +127,7 @@ private enum DeviceContactInfoEntryId: Hashable { } private enum DeviceContactInfoEntry: ItemListNodeEntry { - case info(Int, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer, state: ItemListAvatarAndNameInfoItemState, job: String?, isPlain: Bool) + case info(Int, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer, state: ItemListAvatarAndNameInfoItemState, job: String?, isPlain: Bool, hiddenAvatar: TelegramMediaImageRepresentation?) case invite(Int, PresentationTheme, String) case sendMessage(Int, PresentationTheme, String) @@ -204,8 +209,8 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { static func ==(lhs: DeviceContactInfoEntry, rhs: DeviceContactInfoEntry) -> Bool { switch lhs { - case let .info(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsState, lhsJobSummary, lhsIsPlain): - if case let .info(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsState, rhsJobSummary, rhsIsPlain) = rhs { + case let .info(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsState, lhsJobSummary, lhsIsPlain, lhsHiddenAvatar): + if case let .info(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsState, rhsJobSummary, rhsIsPlain, rhsHiddenAvatar) = rhs { if lhsIndex != rhsIndex { return false } @@ -230,6 +235,9 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { if lhsIsPlain != rhsIsPlain { return false } + if lhsHiddenAvatar != rhsHiddenAvatar { + return false + } return true } else { return false @@ -347,7 +355,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { private var sortIndex: Int { switch self { - case let .info(index, _, _, _, _, _, _, _): + case let .info(index, _, _, _, _, _, _, _, _): return index case let .sendMessage(index, _, _): return index @@ -395,11 +403,14 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DeviceContactInfoControllerArguments switch self { - case let .info(_, _, _, dateTimeFormat, peer, state, jobSummary, _): + case let .info(_, _, _, dateTimeFormat, peer, state, jobSummary, _, hiddenAvatar): return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .contact, peer: EnginePeer(peer), presence: nil, label: jobSummary, memberCount: nil, state: state, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks(withTopInset: false, withExtendedBottomInset: true), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { - }, context: nil, call: nil) + if peer.smallProfileImage != nil { + arguments.openAvatar(peer) + } + }, context: ItemListAvatarAndNameInfoItemContext(hiddenAvatarRepresentation: hiddenAvatar), call: nil) case let .sendMessage(_, _, title): return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: { arguments.performAction(.sendMessage) @@ -614,7 +625,7 @@ private func filteredContactData(contactData: DeviceContactExtendedData, exclude return DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumbers: phoneNumbers), middleName: contactData.middleName, prefix: contactData.prefix, suffix: contactData.suffix, organization: includeJob ? contactData.organization : "", jobTitle: includeJob ? contactData.jobTitle : "", department: includeJob ? contactData.department : "", emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: includeBirthday ? contactData.birthdayDate : nil, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles, note: includeNote ? contactData.note : "") } -private func deviceContactInfoEntries(account: Account, engine: TelegramEngine, presentationData: PresentationData, peer: Peer?, isShare: Bool, shareViaException: Bool, contactData: DeviceContactExtendedData, isContact: Bool, state: DeviceContactInfoState, selecting: Bool, editingPhoneNumbers: Bool) -> [DeviceContactInfoEntry] { +private func deviceContactInfoEntries(account: Account, engine: TelegramEngine, presentationData: PresentationData, peer: Peer?, isShare: Bool, shareViaException: Bool, contactData: DeviceContactExtendedData, isContact: Bool, state: DeviceContactInfoState, selecting: Bool, editingPhoneNumbers: Bool, hiddenAvatar: TelegramMediaImageRepresentation?) -> [DeviceContactInfoEntry] { var entries: [DeviceContactInfoEntry] = [] var editingName: ItemListAvatarAndNameInfoItemName? @@ -652,7 +663,7 @@ private func deviceContactInfoEntries(account: Account, engine: TelegramEngine, firstName = presentationData.strings.Message_Contact } - entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare)) + entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? TelegramUser(id: PeerId(namespace: .max, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: firstName, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare, hiddenAvatar: hiddenAvatar)) if !selecting { if let _ = peer { @@ -856,6 +867,7 @@ public func deviceContactInfoController(context: AccountContext, updatedPresenta var openAddressImpl: ((DeviceContactAddressData) -> Void)? var inviteImpl: (([String]) -> Void)? var dismissImpl: ((Bool) -> Void)? + var openAvatarImpl: ((Peer) -> Void)? let actionsDisposable = DisposableSet() @@ -1042,12 +1054,15 @@ public func deviceContactInfoController(context: AccountContext, updatedPresenta state.addToPrivacyExceptions = value return state } + }, openAvatar: { peer in + openAvatarImpl?(peer) }) + let hiddenAvatarPromise = Promise(nil) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let previousEditingPhoneIds = Atomic?>(value: nil) - let signal = combineLatest(presentationData, statePromise.get(), contactData) - |> map { presentationData, state, peerAndContactData -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(presentationData, statePromise.get(), contactData, hiddenAvatarPromise.get()) + |> map { presentationData, state, peerAndContactData, hiddenAvatar -> (ItemListControllerState, (ItemListNodeState, Any)) in var leftNavigationButton: ItemListNavigationButton? switch subject { case .vcard: @@ -1231,7 +1246,7 @@ public func deviceContactInfoController(context: AccountContext, updatedPresenta focusItemTag = DeviceContactInfoEntryTag.editingPhone(insertedPhoneId) } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deviceContactInfoEntries(account: context.account, engine: context.engine, presentationData: presentationData, peer: peerAndContactData.0, isShare: isShare, shareViaException: shareViaException, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones), style: isShare ? .blocks : .plain, focusItemTag: focusItemTag) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deviceContactInfoEntries(account: context.account, engine: context.engine, presentationData: presentationData, peer: peerAndContactData.0, isShare: isShare, shareViaException: shareViaException, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones, hiddenAvatar: hiddenAvatar), style: isShare ? .blocks : .plain, focusItemTag: focusItemTag) return (controllerState, (listState, arguments)) } @@ -1330,6 +1345,32 @@ public func deviceContactInfoController(context: AccountContext, updatedPresenta } } } + openAvatarImpl = { [weak controller] peer in + let avatarController = AvatarGalleryController(context: context, peer: peer, replaceRootController: { _, _ in + }) + hiddenAvatarPromise.set( + avatarController.hiddenMedia + |> map { entry -> TelegramMediaImageRepresentation? in + return entry?.representations.first?.representation + } + ) + presentControllerImpl?(avatarController, AvatarGalleryControllerPresentationArguments(transitionArguments: { [weak controller] entry in + var transitionNode: ((ASDisplayNode, CGRect, () -> (UIView?, UIView?)), CGRect)? + controller?.forEachItemNode({ itemNode in + if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode { + transitionNode = itemNode.avatarTransitionNode() + } + }) + + if let transitionNode = transitionNode { + return GalleryTransitionArguments(transitionNode: transitionNode.0, addToTransitionSurface: { [weak controller] view in + controller?.view.addSubview(view) + }) + } else { + return nil + } + })) + } return controller } diff --git a/submodules/PeerInfoUI/Sources/GroupStickerPackCurrentItem.swift b/submodules/PeerInfoUI/Sources/GroupStickerPackCurrentItem.swift index c57094dd244..2f32d43117a 100644 --- a/submodules/PeerInfoUI/Sources/GroupStickerPackCurrentItem.swift +++ b/submodules/PeerInfoUI/Sources/GroupStickerPackCurrentItem.swift @@ -232,8 +232,8 @@ class GroupStickerPackCurrentItemNode: ItemListRevealOptionsItemNode { var updatedFetchSignal: Signal? if fileUpdated { if let file = file { - updatedImageSignal = chatMessageSticker(account: item.account, file: file, small: false) - updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(file.resource)) + updatedImageSignal = chatMessageSticker(account: item.account, userLocation: .other, file: file, small: false) + updatedFetchSignal = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(file.resource)) } else { updatedImageSignal = .single({ _ in return nil }) updatedFetchSignal = .complete() diff --git a/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift b/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift index 859550367e5..52bf64fe0d3 100644 --- a/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift +++ b/submodules/PeerOnlineMarkerNode/Sources/PeerOnlineMarkerNode.swift @@ -161,7 +161,7 @@ public final class PeerOnlineMarkerNode: ASDisplayNode { } if animated { - let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.0 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) + let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.001 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) let targetScale: CGFloat = online ? 1.0 : 0.0 if initialScale != targetScale { strongSelf.iconNode.isHidden = false diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 22d5fca5e48..7a737a16338 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -19,6 +19,8 @@ import AppBundle import MusicAlbumArtResources import Svg import RangeSet +import Accelerate + private enum ResourceFileData { case data(Data) @@ -53,8 +55,8 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed return nil } -public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false) -> Signal, NoError> { - if let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { +public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { + if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { enum SizeSource { case miniThumbnail(data: Data) case image(size: Int64) @@ -127,10 +129,12 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe } }) var fetchDisposable: Disposable? - if autoFetchFullSize { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() - } else if useMiniThumbnailIfAvailable { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + if automaticFetch { + if autoFetchFullSize { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() + } else if useMiniThumbnailIfAvailable { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + } } return ActionDisposable { @@ -140,7 +144,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe } } - if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) { + if !forceThumbnail || photoReference.media.immediateThumbnailData == nil, let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let maybeLargestSize = postbox.mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) @@ -159,9 +163,9 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { - fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) + fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } - let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let anyThumbnail: [Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError>] if tryAdditionalRepresentations { @@ -263,7 +267,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaRe } } -private func chatMessageFileDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, fetched: Bool = false) -> Signal, NoError> { +private func chatMessageFileDatas(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, fetched: Bool = false) -> Signal, NoError> { let thumbnailResource = fetched ? nil : smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource let fullSizeResource = fileReference.media.resource @@ -280,7 +284,7 @@ private func chatMessageFileDatas(account: Account, fileReference: FileMediaRefe if !fetched, let _ = decodedThumbnailData { fetchedThumbnail = .single(.local) } else if let thumbnailResource = thumbnailResource { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes)) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource), statsCategory: statsCategoryForFileWithAttributes(fileReference.media.attributes)) } else { fetchedThumbnail = .complete() } @@ -334,7 +338,7 @@ private let thumbnailGenerationMimeTypes: Set = Set([ "image/heic" ]) -private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal, NoError> { +private func chatMessageImageFileThumbnailDatas(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal, NoError> { let thumbnailRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) let thumbnailResource = thumbnailRepresentation?.resource let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) @@ -343,7 +347,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: if let decodedThumbnailData = decodedThumbnailData { if autoFetchFullSizeThumbnail, let thumbnailRepresentation = thumbnailRepresentation, (thumbnailRepresentation.dimensions.width > 200 || thumbnailRepresentation.dimensions.height > 200) { return Signal { subscriber in - let fetchedDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() + let fetchedDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailRepresentation.resource, attemptSynchronously: false).start(next: { next in let data: Data? = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(Tuple(data ?? decodedThumbnailData, nil, false)) @@ -358,7 +362,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: return .single(Tuple(decodedThumbnailData, nil, false)) } } else if let thumbnailResource = thumbnailResource { - let fetchedThumbnail: Signal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) + let fetchedThumbnail: Signal = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource)) return Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in @@ -394,7 +398,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: if let _ = fileReference.media.immediateThumbnailData { fetchedThumbnail = .complete() } else if let thumbnailResource = thumbnailResource { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource)) } else { fetchedThumbnail = .complete() } @@ -440,7 +444,7 @@ private func chatMessageImageFileThumbnailDatas(account: Account, fileReference: return signal } -private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal?, Bool>, NoError> { +private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { let fullSizeResource = fileReference.media.resource var reducedSizeResource: MediaResource? if let videoThumbnail = fileReference.media.videoThumbnails.first { @@ -460,7 +464,7 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal?, Bool>, NoError> in - if maybeData.complete { + if maybeData.complete && !forceThumbnail { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData == nil ? nil : Tuple(loadedData!, maybeData.path), true)) } else { @@ -470,7 +474,7 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef } else if let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { if autoFetchFullSizeThumbnail, let thumbnailRepresentation = thumbnailRepresentation, (thumbnailRepresentation.dimensions.width > 200 || thumbnailRepresentation.dimensions.height > 200) { thumbnail = Signal { subscriber in - let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() + let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailRepresentation.resource), statsCategory: .video).start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in let data: Data? = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(data ?? decodedThumbnailData) @@ -486,7 +490,7 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef } } else if let thumbnailResource = thumbnailResource { thumbnail = Signal { subscriber in - let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video).start() + let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video).start() let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -567,8 +571,8 @@ private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaRef return signal } -public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> Signal { - return chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, autoFetchFullSize: true) +public func rawMessagePhoto(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { + return chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, photoReference: photoReference, autoFetchFullSize: true) |> map { value -> UIImage? in let thumbnailData = value._0 let fullSizeData = value._1 @@ -585,8 +589,8 @@ public func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReferenc } } -public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) +public func chatMessagePhoto(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) |> map { _, _, generate in return generate } @@ -796,7 +800,7 @@ public func chatMessagePhotoInternal(photoData: Signal Signal, NoError> { +private func chatMessagePhotoThumbnailDatas(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, onlyFullSize: Bool = false, forceThumbnail: Bool = false) -> Signal, NoError> { let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))) { @@ -806,11 +810,11 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: Im let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal, NoError> in - if maybeData.complete { + if maybeData.complete, !forceThumbnail { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { - let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -846,8 +850,8 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: Im } } -public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMediaReference, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoThumbnailDatas(account: account, photoReference: photoReference, onlyFullSize: onlyFullSize) +public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, onlyFullSize: Bool = false, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoThumbnailDatas(account: account, userLocation: userLocation, photoReference: photoReference, onlyFullSize: onlyFullSize, forceThumbnail: blurred) return signal |> map { value in let thumbnailData = value._0 @@ -871,7 +875,7 @@ public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMed var fullSizeImage: CGImage? var imageOrientation: UIImage.Orientation = .up - if let fullSizeData = fullSizeData { + if let fullSizeData = fullSizeData, !blurred { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -900,7 +904,7 @@ public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMed var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none @@ -908,6 +912,11 @@ public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMed } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if blurred { + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + blurredThumbnailImage = thumbnailContext.generateImage() } } @@ -939,8 +948,8 @@ public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMed } } -public func chatMessageVideoThumbnail(account: Account, fileReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(postbox: account.postbox, fileReference: fileReference, thumbnailSize: true, autoFetchFullSizeThumbnail: true) +public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) return signal |> map { value in @@ -970,7 +979,7 @@ public func chatMessageVideoThumbnail(account: Account, fileReference: FileMedia var fullSizeImage: CGImage? var imageOrientation: UIImage.Orientation = .up - if let fullSizeData = fullSizeData?._0 { + if let fullSizeData = fullSizeData?._0, !blurred { if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -998,11 +1007,11 @@ public func chatMessageVideoThumbnail(account: Account, fileReference: FileMedia var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { - if max(thumbnailImage.width, thumbnailImage.height) > 200 { + if max(thumbnailImage.width, thumbnailImage.height) > 200 && !blurred { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContextSize = thumbnailSize.aspectFitted(blurred ? CGSize(width: 50.0, height: 50.0) : CGSize(width: 150.0, height: 150.0)) if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none @@ -1010,6 +1019,11 @@ public func chatMessageVideoThumbnail(account: Account, fileReference: FileMedia } imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if blurred { + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + blurredThumbnailImage = thumbnailContext.generateImage() } } @@ -1042,8 +1056,8 @@ public func chatMessageVideoThumbnail(account: Account, fileReference: FileMedia } } -public func chatSecretPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference) +public func chatSecretPhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, ignoreFullSize: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, synchronousLoad: synchronousLoad, forceThumbnail: ignoreFullSize) return signal |> map { value in let thumbnailData = value._0 @@ -1146,6 +1160,8 @@ public func chatSecretPhoto(account: Account, photoReference: ImageMediaReferenc } } + adjustSaturationInContext(context: context, saturation: 1.7) + addCorners(context, arguments: arguments) return context @@ -1153,6 +1169,45 @@ public func chatSecretPhoto(account: Account, photoReference: ImageMediaReferenc } } +public func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) { + var buffer = vImage_Buffer() + buffer.data = context.bytes + buffer.width = UInt(context.size.width * context.scale) + buffer.height = UInt(context.size.height * context.scale) + buffer.rowBytes = context.bytesPerRow + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation = saturation + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) +} + private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [ImageRepresentationWithReference], fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, synchronousLoad: Bool) -> Signal, NoError> { if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = imageRepresentationLargerThan(representations.map({ $0.representation }), size: PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad) @@ -1164,8 +1219,8 @@ private func avatarGalleryThumbnailDatas(postbox: Postbox, representations: [Ima let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(Tuple(nil, loadedData, true)) } else { - let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: representations[smallestIndex].reference, statsCategory: .image) - let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: representations[largestIndex].reference, statsCategory: .image) + let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[smallestIndex].reference, statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference, statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() @@ -1310,7 +1365,7 @@ public func avatarGalleryThumbnailPhoto(account: Account, representations: [Imag } } -public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0 var updatedFullRepresentationSize = fullRepresentationSize if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) { @@ -1318,7 +1373,7 @@ public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaRe updatedFullRepresentationSize = largest.dimensions.cgSize } } - let signal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable) + let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred, automaticFetch: automaticFetch) return signal |> map { value in @@ -1365,7 +1420,7 @@ public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaRe var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { - if useMiniThumbnailIfAvailable { + if useMiniThumbnailIfAvailable && !blurred { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) @@ -1375,10 +1430,15 @@ public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaRe c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } - if !useMiniThumbnailIfAvailable { + if !useMiniThumbnailIfAvailable || blurred { telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) } + if blurred { + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + blurredThumbnailImage = thumbnailContext.generateImage() } } @@ -1398,7 +1458,7 @@ public func mediaGridMessagePhoto(account: Account, photoReference: ImageMediaRe c.setBlendMode(.normal) } - if let fullSizeImage = fullSizeImage { + if let fullSizeImage = fullSizeImage, !blurred { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } @@ -1421,7 +1481,7 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef }, completed: { subscriber.putCompletion() }) - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: videoReference.resourceReference(thumbnailResource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: videoReference.resourceReference(thumbnailResource)).start() return ActionDisposable { data.dispose() fetched.dispose() @@ -1484,17 +1544,17 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef } } -public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor, nilForEmptyResult: nilForEmptyResult, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable) +public func mediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor, nilForEmptyResult: nilForEmptyResult, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable) |> map { return $0.1 } } -public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { +public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { - signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad) + signal = chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad, forceThumbnail: blurred) |> map { value -> Tuple3?, Bool> in let thumbnailData = value._0 let fullSizeData = value._1 @@ -1502,7 +1562,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete) } } else { - signal = chatMessageVideoDatas(postbox: postbox, fileReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail) + signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, fileReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) } return signal @@ -1579,7 +1639,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File var blurredThumbnailImage: UIImage? if let thumbnailImage = thumbnailImage { - if max(thumbnailImage.width, thumbnailImage.height) > Int(min(200.0, min(drawingSize.width, drawingSize.height))) || useMiniThumbnailIfAvailable { + if max(thumbnailImage.width, thumbnailImage.height) > Int(min(200.0, min(drawingSize.width, drawingSize.height))) || useMiniThumbnailIfAvailable, !blurred { blurredThumbnailImage = UIImage(cgImage: thumbnailImage) } else { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) @@ -1592,6 +1652,10 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if blurred { + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + } + var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) @@ -1691,7 +1755,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File c.setBlendMode(.normal) } - if let fullSizeImage = fullSizeImage { + if let fullSizeImage = fullSizeImage, !blurred { c.interpolationQuality = .medium drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) } @@ -1744,9 +1808,9 @@ public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId } } -public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photoReference: ImageMediaReference) -> Signal { +public func standaloneChatMessagePhotoInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { - return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in return .single(type) } @@ -1755,14 +1819,14 @@ public func standaloneChatMessagePhotoInteractiveFetched(account: Account, photo } } -public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { +public func chatMessagePhotoInteractiveFetched(context: AccountContext, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, displayAtSize: Int?, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photoReference.media) { var fetchRange: (Range, MediaBoxFetchPriority)? if let displayAtSize = displayAtSize, let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) { fetchRange = (range, .default) } - return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: photoReference.resourceReference(largestRepresentation.resource), range: fetchRange, statsCategory: .image, reportResultStatus: true) + return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(largestRepresentation.resource), range: fetchRange, statsCategory: .image, reportResultStatus: true) |> mapToSignal { type -> Signal in if case .remote = type, let peerType = storeToDownloadsPeerType { return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType) @@ -1788,18 +1852,18 @@ public func chatMessagePhotoCancelInteractiveFetch(account: Account, photoRefere } } -public func chatMessageWebFileInteractiveFetched(account: Account, image: TelegramMediaWebFile) -> Signal { - return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .standalone(resource: image.resource), statsCategory: .image) +public func chatMessageWebFileInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, image: TelegramMediaWebFile) -> Signal { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: .standalone(resource: image.resource), statsCategory: .image) } public func chatMessageWebFileCancelInteractiveFetch(account: Account, image: TelegramMediaWebFile) { return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) } -public func chatWebpageSnippetFileData(account: Account, mediaReference: AnyMediaReference, resource: MediaResource) -> Signal { +public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource, automaticFetch: Bool = true) -> Signal { let resourceData = account.postbox.mediaBox.resourceData(resource) - |> map { next in - return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + |> map { next in + return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } return Signal { subscriber in @@ -1810,12 +1874,21 @@ public func chatWebpageSnippetFileData(account: Account, mediaReference: AnyMedi }, completed: { subscriber.putCompletion() })) - disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: mediaReference.resourceReference(resource)).start()) + if automaticFetch { + var userContentType: MediaResourceUserContentType = .other + if let file = mediaReference.media as? TelegramMediaFile { + userContentType = MediaResourceUserContentType(file: file) + } else if let _ = mediaReference.media as? TelegramMediaImage { + userContentType = .image + } + + disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start()) + } return disposable } } -public func chatWebpageSnippetPhotoData(account: Account, photoReference: ImageMediaReference) -> Signal { +public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal { if let closestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: 120, height: 120)) { let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in @@ -1830,7 +1903,7 @@ public func chatWebpageSnippetPhotoData(account: Account, photoReference: ImageM }, completed: { subscriber.putCompletion() })) - disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: photoReference.resourceReference(closestRepresentation.resource)).start()) + disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(closestRepresentation.resource)).start()) return disposable } } else { @@ -1838,8 +1911,8 @@ public func chatWebpageSnippetPhotoData(account: Account, photoReference: ImageM } } -public func chatWebpageSnippetFile(account: Account, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatWebpageSnippetFileData(account: account, mediaReference: mediaReference, resource: representation.resource) +public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch) return signal |> map { fullSizeData in return { arguments in @@ -1853,7 +1926,42 @@ public func chatWebpageSnippetFile(account: Account, mediaReference: AnyMediaRef } } - if let fullSizeImage = fullSizeImage { + var blurredImage: UIImage? + if fullSizeImage == nil { + var immediateThumbnailData: Data? + if let file = mediaReference.media as? TelegramMediaFile { + immediateThumbnailData = file.immediateThumbnailData + } else if let image = mediaReference.media as? TelegramMediaImage { + immediateThumbnailData = image.immediateThumbnailData + } + + if let decodedThumbnailData = immediateThumbnailData.flatMap(decodeTinyThumbnail), let imageSource = CGImageSourceCreateWithData(decodedThumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let thumbnailSize = CGSize(width: image.width, height: image.height) + let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) + if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) + if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) { + thumbnailContext2.withFlippedContext { c in + c.interpolationQuality = .none + if let image = thumbnailContext.generateImage()?.cgImage { + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) + } + } + imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) + + blurredImage = thumbnailContext2.generateImage() + } + } + } + } + + if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) { guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } @@ -1904,8 +2012,8 @@ public func chatWebpageSnippetFile(account: Account, mediaReference: AnyMediaRef } } -public func chatWebpageSnippetPhoto(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatWebpageSnippetPhotoData(account: account, photoReference: photoReference) +public func chatWebpageSnippetPhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatWebpageSnippetPhotoData(account: account, userLocation: userLocation, photoReference: photoReference) return signal |> map { fullSizeData in return { arguments in @@ -1949,21 +2057,21 @@ public func chatWebpageSnippetPhoto(account: Account, photoReference: ImageMedia } } -public func chatMessageVideo(postbox: Postbox, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return mediaGridMessageVideo(postbox: postbox, videoReference: videoReference) +public func chatMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return mediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: videoReference) } -private func chatSecretMessageVideoData(account: Account, fileReference: FileMediaReference) -> Signal { +private func chatSecretMessageVideoData(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal { if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource - let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(thumbnailResource)) let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: synchronousLoad).start(next: { next in let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) subscriber.putNext(data ?? decodedThumbnailData) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -1979,8 +2087,8 @@ private func chatSecretMessageVideoData(account: Account, fileReference: FileMed } } -public func chatSecretMessageVideo(account: Account, videoReference: FileMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatSecretMessageVideoData(account: account, fileReference: videoReference) +public func chatSecretMessageVideo(account: Account, userLocation: MediaResourceUserLocation, videoReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatSecretMessageVideoData(account: account, userLocation: userLocation, fileReference: videoReference, synchronousLoad: synchronousLoad) return signal |> map { thumbnailData in @@ -2138,12 +2246,12 @@ public func drawImage(context: CGContext, image: CGImage, orientation: UIImage.O } } -public func chatMessageImageFile(account: Account, fileReference: FileMediaReference, thumbnail: Bool, fetched: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageImageFile(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, thumbnail: Bool, fetched: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { - signal = chatMessageImageFileThumbnailDatas(account: account, fileReference: fileReference, autoFetchFullSizeThumbnail: true) + signal = chatMessageImageFileThumbnailDatas(account: account, userLocation: userLocation, fileReference: fileReference, autoFetchFullSizeThumbnail: true) } else { - signal = chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) + signal = chatMessageFileDatas(account: account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: fetched) } return signal @@ -2276,8 +2384,8 @@ public func chatMessageImageFile(account: Account, fileReference: FileMediaRefer } } -public func instantPageImageFile(account: Account, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false, fetched: fetched) +public func instantPageImageFile(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, fetched: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessageFileDatas(account: account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: fetched) |> map { value in let fullSizePath = value._1 let fullSizeComplete = value._2 @@ -2404,9 +2512,9 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[smallestIndex].reference) } - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[largestIndex].reference) let thumbnail = Signal { subscriber in if let decodedThumbnailData = decodedThumbnailData { @@ -2789,7 +2897,7 @@ public func playerAlbumArt(postbox: Postbox, engine: TelegramEngine, fileReferen if let fileReference = fileReference, let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource - let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) + let fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() diff --git a/submodules/Postbox/BUILD b/submodules/Postbox/BUILD index 0a8112e48f5..4e32c33a457 100644 --- a/submodules/Postbox/BUILD +++ b/submodules/Postbox/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/StringTransliteration:StringTransliteration", "//submodules/ManagedFile:ManagedFile", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/CryptoUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/Postbox/Package.swift b/submodules/Postbox/Package.swift index d9bcd9b6e88..049d1412959 100644 --- a/submodules/Postbox/Package.swift +++ b/submodules/Postbox/Package.swift @@ -22,6 +22,7 @@ let package = Package( .package(name: "ManagedFile", path: "../ManagedFile"), .package(name: "RangeSet", path: "../Utils/RangeSet"), .package(name: "SSignalKit", path: "../SSignalKit"), + .package(name: "CryptoUtils", path: "../CryptoUtils") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -34,6 +35,7 @@ let package = Package( .product(name: "RangeSet", package: "RangeSet", condition: nil), .product(name: "sqlcipher", package: "sqlcipher", condition: nil), .product(name: "StringTransliteration", package: "StringTransliteration", condition: nil), + .product(name: "CryptoUtils", package: "CryptoUtils", condition: nil), .product(name: "Crc32", package: "Crc32", condition: nil)], path: "Sources"), ] diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 90409848fb3..60014d11616 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -42,9 +42,9 @@ public enum FetchResourceError { case generic } -private struct ResourceStorePaths { - let partial: String - let complete: String +public struct ResourceStorePaths { + public let partial: String + public let complete: String } public struct MediaResourceData { @@ -144,6 +144,8 @@ public final class MediaBox { private let cacheQueue = Queue() private let timeBasedCleanup: TimeBasedCleanup + public let storageBox: StorageBox + private let didRemoveResourcesPipe = ValuePipe() public var didRemoveResources: Signal { return .single(Void()) |> then(self.didRemoveResourcesPipe.signal()) @@ -187,23 +189,38 @@ public final class MediaBox { public init(basePath: String) { self.basePath = basePath - self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [ - self.basePath, + self.storageBox = StorageBox(logger: StorageBox.Logger(impl: { string in + postboxLog(string) + }), basePath: basePath + "/storage") + + self.timeBasedCleanup = TimeBasedCleanup(storageBox: self.storageBox, generalPaths: [ self.basePath + "/cache", self.basePath + "/animation-cache" - ], shortLivedPaths: [ + ], totalSizeBasedPath: self.basePath, shortLivedPaths: [ self.basePath + "/short-cache" ]) self.dataFileManager = MediaBoxFileManager(queue: self.dataQueue) let _ = self.ensureDirectoryCreated + + //self.updateResourceIndex() } public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { self.timeBasedCleanup.setMaxStoreTimes(general: general, shortLived: shortLived, gigabytesLimit: gigabytesLimit) } + public static func idForFileName(name: String) -> String { + if name.hasSuffix("_partial.meta") { + return String(name[name.startIndex ..< name.index(name.endIndex, offsetBy: -13)]) + } else if name.hasSuffix("_partial") { + return String(name[name.startIndex ..< name.index(name.endIndex, offsetBy: -8)]) + } else { + return name + } + } + private func fileNameForId(_ id: MediaResourceId) -> String { return "\(id.stringRepresentation)" } @@ -216,10 +233,21 @@ public final class MediaBox { return "\(self.basePath)/\(fileNameForId(id))" } - private func storePathsForId(_ id: MediaResourceId) -> ResourceStorePaths { + public func storePathsForId(_ id: MediaResourceId) -> ResourceStorePaths { return ResourceStorePaths(partial: "\(self.basePath)/\(fileNameForId(id))_partial", complete: "\(self.basePath)/\(fileNameForId(id))") } + public func fileSizeForId(_ id: MediaResourceId) -> Int64 { + let paths = self.storePathsForId(id) + if let size = fileSize(paths.complete, useTotalFileAllocatedSize: false) { + return size + } else if let size = fileSize(paths.partial, useTotalFileAllocatedSize: true) { + return size + } else { + return 0 + } + } + private func fileNamesForId(_ id: MediaResourceId) -> ResourceStorePaths { return ResourceStorePaths(partial: "\(fileNameForId(id))_partial", complete: "\(fileNameForId(id))") } @@ -543,7 +571,7 @@ public final class MediaBox { paths.partial, paths.partial + ".meta" ]) - if let fileContext = MediaBoxFileContext(queue: self.dataQueue, manager: self.dataFileManager, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { + if let fileContext = MediaBoxFileContext(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") { context = fileContext self.fileContexts[resourceId] = fileContext } else { @@ -580,6 +608,19 @@ public final class MediaBox { return } + if let parameters = parameters, let location = parameters.location { + var messageNamespace: Int32 = 0 + var messageIdValue: Int32 = 0 + if let messageId = location.messageId { + messageNamespace = messageId.namespace + messageIdValue = messageId.id + } + + self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters.contentType.rawValue) + } else { + self.storageBox.add(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters?.contentType.rawValue ?? 0) + } + guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else { subscriber.putCompletion() return @@ -743,6 +784,19 @@ public final class MediaBox { self.dataQueue.async { let paths = self.storePathsForId(resource.id) + if let parameters = parameters, let location = parameters.location { + var messageNamespace: Int32 = 0 + var messageIdValue: Int32 = 0 + if let messageId = location.messageId { + messageNamespace = messageId.namespace + messageIdValue = messageId.id + } + + self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters.contentType.rawValue) + } else { + self.storageBox.add(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters?.contentType.rawValue ?? 0) + } + if let _ = fileSize(paths.complete) { if implNext { subscriber.putNext(.local) @@ -1175,6 +1229,35 @@ public final class MediaBox { } } + public func resourceUsage(id: MediaResourceId) -> Int64 { + let paths = self.storePathsForId(id) + if let size = fileSize(paths.complete) { + return Int64(size) + } else if let size = fileSize(paths.partial, useTotalFileAllocatedSize: true) { + return Int64(size) + } else { + return 0 + } + } + + public func resourceUsageWithInfo(id: MediaResourceId) -> Int32 { + let paths = self.storePathsForId(id) + + var value = stat() + + if stat(paths.complete, &value) == 0 { + return Int32(value.st_mtimespec.tv_sec) + } + + value = stat() + + if stat(paths.partial, &value) == 0 { + return Int32(value.st_mtimespec.tv_sec) + } + + return 0 + } + public func collectResourceCacheUsage(_ ids: [MediaResourceId]) -> Signal<[MediaResourceId: Int64], NoError> { return Signal { subscriber in self.dataQueue.async { @@ -1195,6 +1278,192 @@ public final class MediaBox { } } + public func updateResourceIndex(lowImpact: Bool, completion: @escaping () -> Void) -> Disposable { + let basePath = self.basePath + let storageBox = self.storageBox + + var isCancelled: Bool = false + + let processQueue = Queue(name: "UpdateResourceIndex", qos: .background) + processQueue.async { + if isCancelled { + return + } + + let scanContext = ScanFilesContext(path: basePath) + + func processStale(nextId: Data?) { + let _ = (storageBox.enumerateItems(startingWith: nextId, limit: 1000) + |> deliverOn(processQueue)).start(next: { ids, realNextId in + var staleIds: [Data] = [] + + for id in ids { + if let name = String(data: id, encoding: .utf8) { + if self.resourceUsage(id: MediaResourceId(name)) == 0 { + staleIds.append(id) + } + } else { + staleIds.append(id) + } + } + + if !staleIds.isEmpty { + storageBox.remove(ids: staleIds) + } + + if realNextId == nil { + completion() + } else { + if lowImpact { + processQueue.after(0.4, { + processStale(nextId: realNextId) + }) + } else { + processStale(nextId: realNextId) + } + } + }) + } + + func processNext() { + processQueue.async { + if isCancelled { + return + } + + let results = scanContext.nextBatch(count: 32000) + if results.isEmpty { + processStale(nextId: nil) + return + } + + storageBox.addEmptyReferencesIfNotReferenced(ids: results.map { name -> (id: Data, size: Int64) in + let resourceId = MediaBox.idForFileName(name: name) + let paths = self.storePathsForId(MediaResourceId(resourceId)) + var size: Int64 = 0 + if let value = fileSize(paths.complete) { + size = value + } else if let value = fileSize(paths.partial) { + size = value + } + return (resourceId.data(using: .utf8)!, size) + }, contentType: MediaResourceUserContentType.other.rawValue, completion: { addedCount in + if addedCount != 0 { + postboxLog("UpdateResourceIndex: added \(addedCount) unreferenced ids") + } + + if lowImpact { + processQueue.after(0.4, { + processNext() + }) + } else { + processNext() + } + }) + } + } + + processNext() + } + + return ActionDisposable { + isCancelled = true + } + } + + public func collectAllResourceUsage() -> Signal<[(id: String?, path: String, size: Int64)], NoError> { + return Signal { subscriber in + self.dataQueue.async { + var result: [(id: String?, path: String, size: Int64)] = [] + + var fileIds = Set() + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.fileSizeKey, .fileResourceIdentifierKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let fileId = (try? url.resourceValues(forKeys: Set([.fileResourceIdentifierKey])))?.fileResourceIdentifier as? Data { + if fileIds.contains(fileId) { + //paths.append(url.lastPathComponent) + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + fileIds.insert(fileId) + result.append((id: MediaBox.idForFileName(name: url.lastPathComponent), path: url.lastPathComponent, size: Int64(value))) + //paths.append(url.lastPathComponent) + } + } + } + } + } + + /*var cacheResult: Int64 = 0 + + var excludePrefixes = Set() + for id in excludeIds { + let cachedRepresentationPrefix = self.fileNameForId(id) + + excludePrefixes.insert(cachedRepresentationPrefix) + } + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("cache/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + } + + func processRecursive(directoryPath: String, subdirectoryPath: String) { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory { + processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)") + } else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("\(subdirectoryPath)/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + } + } + + processRecursive(directoryPath: self.basePath + "/animation-cache", subdirectoryPath: "animation-cache") + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) { + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + paths.append("short-cache/" + url.lastPathComponent) + cacheResult += Int64(value) + } + } + } + }*/ + + subscriber.putNext(result) + subscriber.putCompletion() + } + return EmptyDisposable + } + } + public func collectOtherResourceUsage(excludeIds: Set, combinedExcludeIds: Set) -> Signal<(Int64, [String], Int64), NoError> { return Signal { subscriber in self.dataQueue.async { @@ -1347,7 +1616,7 @@ public final class MediaBox { } } - public func removeCachedResources(_ ids: Set, force: Bool = false, notify: Bool = false) -> Signal { + public func removeCachedResources(_ ids: [MediaResourceId], force: Bool = false, notify: Bool = false) -> Signal { return Signal { subscriber in self.dataQueue.async { let uniqueIds = Set(ids.map { $0.stringRepresentation }) @@ -1459,3 +1728,120 @@ public final class MediaBox { } } + +private final class ScanFilesContext { + private let path: String + private var dirHandle: UnsafeMutablePointer? + private let pathBuffer: UnsafeMutablePointer + + init(path: String) { + self.path = path + self.dirHandle = opendir(path) + self.pathBuffer = malloc(2048).assumingMemoryBound(to: Int8.self) + } + + deinit { + if let dirHandle = self.dirHandle { + closedir(dirHandle) + } + free(self.pathBuffer) + } + + func nextBatch(count: Int) -> [String] { + guard let dirHandle = self.dirHandle else { + return [] + } + + var result: [String] = [] + + while true { + guard let dirp = readdir(dirHandle) else { + closedir(dirHandle) + self.dirHandle = nil + break + } + + if dirp.pointee.d_type != DT_REG { + continue + } + + if strncmp(&dirp.pointee.d_name.0, ".", 1024) == 0 { + continue + } + if strncmp(&dirp.pointee.d_name.0, "..", 1024) == 0 { + continue + } + + strncpy(self.pathBuffer, self.path, 1024) + strncat(self.pathBuffer, "/", 1024) + strncat(self.pathBuffer, &dirp.pointee.d_name.0, 1024) + + //puts(pathBuffer) + //puts("\n") + + var value = stat() + if stat(self.pathBuffer, &value) == 0 { + if let itemPath = String(data: Data(bytes: &dirp.pointee.d_name.0, count: Int(dirp.pointee.d_namlen)), encoding: .utf8) { + result.append(itemPath) + } + + /*result.totalSize += UInt64(value.st_size) + inodes.append(InodeInfo( + inode: value.st_ino, + timestamp: Int32(clamping: value.st_mtimespec.tv_sec), + size: UInt32(clamping: value.st_size) + ))*/ + } + } + + return result + } +} + +/*private func scanFiles(at path: String, inodes: inout [InodeInfo]) -> ScanFilesResult { + var result = ScanFilesResult() + + if let dp = opendir(path) { + let pathBuffer = malloc(2048).assumingMemoryBound(to: Int8.self) + defer { + free(pathBuffer) + } + + while true { + guard let dirp = readdir(dp) else { + break + } + + if strncmp(&dirp.pointee.d_name.0, ".", 1024) == 0 { + continue + } + if strncmp(&dirp.pointee.d_name.0, "..", 1024) == 0 { + continue + } + strncpy(pathBuffer, path, 1024) + strncat(pathBuffer, "/", 1024) + strncat(pathBuffer, &dirp.pointee.d_name.0, 1024) + + //puts(pathBuffer) + //puts("\n") + + var value = stat() + if stat(pathBuffer, &value) == 0 { + if value.st_mtimespec.tv_sec < minTimestamp { + unlink(pathBuffer) + result.unlinkedCount += 1 + } else { + result.totalSize += UInt64(value.st_size) + inodes.append(InodeInfo( + inode: value.st_ino, + timestamp: Int32(clamping: value.st_mtimespec.tv_sec), + size: UInt32(clamping: value.st_size) + )) + } + } + } + closedir(dp) + } + + return result +}*/ diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index e05d2e2e015..686e2dd4449 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -460,6 +460,8 @@ private class MediaBoxPartialFileDataRequest { final class MediaBoxPartialFile { private let queue: Queue private let manager: MediaBoxFileManager + private let storageBox: StorageBox + private let resourceId: Data private let path: String private let metaPath: String private let completePath: String @@ -476,9 +478,12 @@ final class MediaBoxPartialFile { private var currentFetch: (Promise<[(Range, MediaBoxFetchPriority)]>, Disposable)? private var processedAtLeastOneFetch: Bool = false - init?(queue: Queue, manager: MediaBoxFileManager, path: String, metaPath: String, completePath: String, completed: @escaping (Int64) -> Void) { + init?(queue: Queue, manager: MediaBoxFileManager, storageBox: StorageBox, resourceId: Data, path: String, metaPath: String, completePath: String, completed: @escaping (Int64) -> Void) { assert(queue.isCurrent()) self.manager = manager + self.storageBox = storageBox + self.resourceId = resourceId + if let fd = manager.open(path: path, mode: .readwrite) { self.queue = queue self.path = path @@ -504,6 +509,7 @@ final class MediaBoxPartialFile { } else { self.fileMap = MediaBoxFileMap() } + self.storageBox.update(id: self.resourceId, size: self.fileMap.sum) self.missingRanges = MediaBoxFileMissingRanges() } else { return nil @@ -586,6 +592,8 @@ final class MediaBoxPartialFile { } self.statusRequests.removeAll() + self.storageBox.update(id: self.resourceId, size: self.fileMap.sum) + self.completed(self.fileMap.sum) } else { assertionFailure() @@ -629,6 +637,8 @@ final class MediaBoxPartialFile { } self.statusRequests.removeAll() + self.storageBox.update(id: self.resourceId, size: size) + self.completed(size) } else { assertionFailure() @@ -675,6 +685,8 @@ final class MediaBoxPartialFile { self.fileMap.fill(range) self.fileMap.serialize(manager: self.manager, to: self.metaPath) + self.storageBox.update(id: self.resourceId, size: self.fileMap.sum) + self.checkDataRequestsAfterFill(range: range) } @@ -1184,7 +1196,7 @@ final class MediaBoxFileContext { return self.references.isEmpty } - init?(queue: Queue, manager: MediaBoxFileManager, path: String, partialPath: String, metaPath: String) { + init?(queue: Queue, manager: MediaBoxFileManager, storageBox: StorageBox, resourceId: Data, path: String, partialPath: String, metaPath: String) { assert(queue.isCurrent()) self.queue = queue @@ -1195,7 +1207,7 @@ final class MediaBoxFileContext { var completeImpl: ((Int64) -> Void)? if let size = fileSize(path) { self.content = .complete(path, size) - } else if let file = MediaBoxPartialFile(queue: queue, manager: manager, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in + } else if let file = MediaBoxPartialFile(queue: queue, manager: manager, storageBox: storageBox, resourceId: resourceId, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in completeImpl?(size) }) { self.content = .partial(file) diff --git a/submodules/Postbox/Sources/MediaResource.swift b/submodules/Postbox/Sources/MediaResource.swift index e4d4f2d773c..272ef949bdd 100644 --- a/submodules/Postbox/Sources/MediaResource.swift +++ b/submodules/Postbox/Sources/MediaResource.swift @@ -39,14 +39,38 @@ public protocol MediaResourceFetchTag { public protocol MediaResourceFetchInfo { } +public final class MediaResourceStorageLocation { + public let peerId: PeerId + public let messageId: MessageId? + + public init(peerId: PeerId, messageId: MessageId?) { + self.peerId = peerId + self.messageId = messageId + } +} + +public enum MediaResourceUserContentType: UInt8, Equatable { + case other = 0 + case image = 1 + case video = 2 + case audio = 3 + case file = 4 + case sticker = 6 + case avatar = 7 +} + public struct MediaResourceFetchParameters { public let tag: MediaResourceFetchTag? public let info: MediaResourceFetchInfo? + public let location: MediaResourceStorageLocation? + public let contentType: MediaResourceUserContentType public let isRandomAccessAllowed: Bool - public init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?, isRandomAccessAllowed: Bool) { + public init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?, location: MediaResourceStorageLocation?, contentType: MediaResourceUserContentType, isRandomAccessAllowed: Bool) { self.tag = tag self.info = info + self.location = location + self.contentType = contentType self.isRandomAccessAllowed = isRandomAccessAllowed } } diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index 06e4c7da0f9..a806738e5fe 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -3010,6 +3010,64 @@ final class MessageHistoryTable: Table { return (result, mediaRefs, count == 0 ? nil : lastIndex) } + func enumerateMediaMessages(lowerBound: MessageIndex?, upperBound: MessageIndex?, limit: Int) -> (messagesByMediaId: [MediaId: [MessageId]], mediaMap: [MediaId: Media], nextLowerBound: MessageIndex?) { + var messagesByMediaId: [MediaId: [MessageId]] = [:] + var mediaRefs: [MediaId: Media] = [:] + var lastIndex: MessageIndex? + var count = 0 + self.valueBox.range(self.table, start: self.key(lowerBound == nil ? MessageIndex.absoluteLowerBound() : lowerBound!), end: self.key(upperBound == nil ? MessageIndex.absoluteUpperBound() : upperBound!), values: { key, value in + count += 1 + + let entry = self.readIntermediateEntry(key, value: value) + lastIndex = entry.message.index + + let message = entry.message + + if let upperBound = upperBound, message.id.peerId != upperBound.id.peerId { + return true + } + + var parsedMedia: [Media] = [] + + let embeddedMediaData = message.embeddedMediaData.sharedBufferNoCopy() + if embeddedMediaData.length > 4 { + var embeddedMediaCount: Int32 = 0 + embeddedMediaData.read(&embeddedMediaCount, offset: 0, length: 4) + for _ in 0 ..< embeddedMediaCount { + var mediaLength: Int32 = 0 + embeddedMediaData.read(&mediaLength, offset: 0, length: 4) + if let media = PostboxDecoder(buffer: MemoryBuffer(memory: embeddedMediaData.memory + embeddedMediaData.offset, capacity: Int(mediaLength), length: Int(mediaLength), freeWhenDone: false)).decodeRootObject() as? Media { + parsedMedia.append(media) + } + embeddedMediaData.skip(Int(mediaLength)) + } + } + + for mediaId in message.referencedMedia { + if let media = self.messageMediaTable.get(mediaId, embedded: { _, _ in + return nil + })?.1 { + parsedMedia.append(media) + } + } + + for media in parsedMedia { + if let id = media.id { + mediaRefs[id] = media + if let current = messagesByMediaId[id] { + if !current.contains(message.id) { + messagesByMediaId[id]?.append(message.id) + } + } else { + messagesByMediaId[id] = [message.id] + } + } + } + return true + }, limit: limit) + return (messagesByMediaId, mediaRefs, count == 0 ? nil : lastIndex) + } + func fetch(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags?, threadId: Int64?, from fromIndex: MessageIndex, includeFrom: Bool, to toIndex: MessageIndex, ignoreMessagesInTimestampRange: ClosedRange?, limit: Int) -> [IntermediateMessage] { precondition(fromIndex.id.peerId == toIndex.id.peerId) precondition(fromIndex.id.namespace == toIndex.id.namespace) diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index e03d91fa8d1..f060ab86aa9 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -922,6 +922,15 @@ public final class Transaction { return count } + public func enumerateMediaMessages(lowerBound: MessageIndex?, upperBound: MessageIndex?, limit: Int) -> (messagesByMediaId: [MediaId: [MessageId]], mediaMap: [MediaId: Media], nextLowerBound: MessageIndex?) { + assert(!self.disposed) + if let postbox = self.postbox { + return postbox.messageHistoryTable.enumerateMediaMessages(lowerBound: lowerBound, upperBound: upperBound, limit: limit) + } else { + return ([:], [:], nil) + } + } + public func enumerateMedia(lowerBound: MessageIndex?, upperBound: MessageIndex?, limit: Int) -> ([PeerId: Set], [MediaId: Media], MessageIndex?) { assert(!self.disposed) if let postbox = self.postbox { diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index f26f5e46501..db830a6c37d 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -11,6 +11,7 @@ private struct SqliteValueBoxTable { } let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) +let SQLITE_PREPARE_PERSISTENT: UInt32 = 1 private func checkTableKey(_ table: ValueBoxTable, _ key: ValueBoxKey) { switch table.keyType { @@ -191,6 +192,8 @@ public final class SqliteValueBox: ValueBox { private var updateStatements: [Int32 : SqlitePreparedStatement] = [:] private var insertOrReplacePrimaryKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var insertOrReplaceIndexKeyStatements: [Int32 : SqlitePreparedStatement] = [:] + private var insertOrIgnorePrimaryKeyStatements: [Int32 : SqlitePreparedStatement] = [:] + private var insertOrIgnoreIndexKeyStatements: [Int32 : SqlitePreparedStatement] = [:] private var deleteStatements: [Int32 : SqlitePreparedStatement] = [:] private var moveStatements: [Int32 : SqlitePreparedStatement] = [:] private var copyStatements: [TablePairKey : SqlitePreparedStatement] = [:] @@ -732,7 +735,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT value FROM t\(table.id) WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT value FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.getStatements[table.id] = preparedStatement @@ -761,7 +764,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.getRowIdStatements[table.id] = preparedStatement @@ -791,7 +794,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeKeyAscStatementsLimit[table.id] = preparedStatement @@ -824,7 +827,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeKeyAscStatementsNoLimit[table.id] = preparedStatement @@ -855,7 +858,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeKeyDescStatementsLimit[table.id] = preparedStatement @@ -887,7 +890,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeKeyDescStatementsNoLimit[table.id] = preparedStatement @@ -919,7 +922,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM t\(table.id) WHERE key >= ? AND key <= ?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM t\(table.id) WHERE key >= ? AND key <= ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.deleteRangeStatements[table.id] = preparedStatement @@ -951,7 +954,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeValueAscStatementsLimit[table.id] = preparedStatement @@ -983,7 +986,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeValueAscStatementsNoLimit[table.id] = preparedStatement @@ -1015,7 +1018,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC LIMIT ?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeValueDescStatementsLimit[table.id] = preparedStatement @@ -1048,7 +1051,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) WHERE key > ? AND key < ? ORDER BY key DESC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.rangeValueDescStatementsNoLimit[table.id] = preparedStatement @@ -1078,7 +1081,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key, value FROM t\(table.id) ORDER BY key ASC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key, value FROM t\(table.id) ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.scanStatements[table.id] = preparedStatement @@ -1099,7 +1102,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table.id) ORDER BY key ASC", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT key FROM t\(table.id) ORDER BY key ASC", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.scanKeysStatements[table.id] = preparedStatement @@ -1121,7 +1124,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT rowid FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.existsStatements[table.id] = preparedStatement @@ -1150,7 +1153,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "UPDATE t\(table.id) SET value=? WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "UPDATE t\(table.id) SET value=? WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.updateStatements[table.id] = preparedStatement @@ -1181,7 +1184,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" // MARK: Nicegram CrashLogging @@ -1202,7 +1205,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { let errorText = self.database.currentError() ?? "Unknown error" // MARK: Nicegram CrashLogging @@ -1237,6 +1240,59 @@ public final class SqliteValueBox: ValueBox { return resultStatement } + private func insertOrIgnoreStatement(_ table: SqliteValueBoxTable, key: ValueBoxKey, value: MemoryBuffer) -> SqlitePreparedStatement { + precondition(self.queue.isCurrent()) + checkTableKey(table.table, key) + + let resultStatement: SqlitePreparedStatement + + if table.table.keyType == .int64 || table.hasPrimaryKey { + if let statement = self.insertOrIgnorePrimaryKeyStatements[table.table.id] { + resultStatement = statement + } else { + var statement: OpaquePointer? = nil + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?) ON CONFLICT(key) DO NOTHING", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) + if status != SQLITE_OK { + let errorText = self.database.currentError() ?? "Unknown error" + preconditionFailure(errorText) + } + let preparedStatement = SqlitePreparedStatement(statement: statement) + self.insertOrIgnorePrimaryKeyStatements[table.table.id] = preparedStatement + resultStatement = preparedStatement + } + } else { + if let statement = self.insertOrIgnoreIndexKeyStatements[table.table.id] { + resultStatement = statement + } else { + var statement: OpaquePointer? = nil + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(table.table.id) (key, value) VALUES(?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) + if status != SQLITE_OK { + let errorText = self.database.currentError() ?? "Unknown error" + preconditionFailure(errorText) + } + let preparedStatement = SqlitePreparedStatement(statement: statement) + self.insertOrIgnorePrimaryKeyStatements[table.table.id] = preparedStatement + resultStatement = preparedStatement + } + } + + resultStatement.reset() + + switch table.table.keyType { + case .binary: + resultStatement.bind(1, data: key.memory, length: key.length) + case .int64: + resultStatement.bind(1, number: key.getInt64(0)) + } + if value.length == 0 { + resultStatement.bindNull(2) + } else { + resultStatement.bind(2, data: value.memory, length: value.length) + } + + return resultStatement + } + private func deleteStatement(_ table: ValueBoxTable, key: ValueBoxKey) -> SqlitePreparedStatement { precondition(self.queue.isCurrent()) checkTableKey(table, key) @@ -1247,7 +1303,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM t\(table.id) WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM t\(table.id) WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.deleteStatements[table.id] = preparedStatement @@ -1277,7 +1333,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "UPDATE t\(table.id) SET key=? WHERE key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "UPDATE t\(table.id) SET key=? WHERE key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.moveStatements[table.id] = preparedStatement @@ -1311,7 +1367,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO t\(toTable.id) (key, value) SELECT ?, t\(fromTable.id).value FROM t\(fromTable.id) WHERE t\(fromTable.id).key=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO t\(toTable.id) (key, value) SELECT ?, t\(fromTable.id).value FROM t\(fromTable.id) WHERE t\(fromTable.id).key=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.copyStatements[TablePairKey(table1: fromTable.id, table2: toTable.id)] = preparedStatement @@ -1346,7 +1402,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "INSERT INTO ft\(table.id) (collectionId, itemId, contents, tags) VALUES(?, ?, ?, ?)", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "INSERT INTO ft\(table.id) (collectionId, itemId, contents, tags) VALUES(?, ?, ?, ?)", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.fullTextInsertStatements[table.id] = preparedStatement @@ -1385,7 +1441,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "DELETE FROM ft\(table.id) WHERE itemId=?", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "DELETE FROM ft\(table.id) WHERE itemId=?", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.fullTextDeleteStatements[table.id] = preparedStatement @@ -1409,7 +1465,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\"'", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) if status != SQLITE_OK { self.printError() assertionFailure() @@ -1436,7 +1492,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\"'", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.fullTextMatchCollectionStatements[table.id] = preparedStatement @@ -1465,7 +1521,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: OpaquePointer? = nil - let status = sqlite3_prepare_v2(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\" AND tags:\"' || ? || '\"'", -1, &statement, nil) + let status = sqlite3_prepare_v3(self.database.handle, "SELECT collectionId, itemId FROM ft\(table.id) WHERE ft\(table.id) MATCH 'contents:\"' || ? || '\" AND collectionId:\"' || ? || '\" AND tags:\"' || ? || '\"'", -1, SQLITE_PREPARE_PERSISTENT, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) self.fullTextMatchCollectionTagsStatements[table.id] = preparedStatement @@ -1864,6 +1920,30 @@ public final class SqliteValueBox: ValueBox { } } + public func setOrIgnore(_ table: ValueBoxTable, key: ValueBoxKey, value: MemoryBuffer) { + precondition(self.queue.isCurrent()) + let sqliteTable = self.checkTable(table) + + if sqliteTable.hasPrimaryKey { + let statement = self.insertOrIgnoreStatement(sqliteTable, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } else { + if self.exists(table, key: key) { + let statement = self.updateStatement(table, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } else { + let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { + } + statement.reset() + } + } + } + public func remove(_ table: ValueBoxTable, key: ValueBoxKey, secure: Bool) { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { @@ -2125,6 +2205,16 @@ public final class SqliteValueBox: ValueBox { } self.insertOrReplacePrimaryKeyStatements.removeAll() + for (_, statement) in self.insertOrIgnoreIndexKeyStatements { + statement.destroy() + } + self.insertOrIgnoreIndexKeyStatements.removeAll() + + for (_, statement) in self.insertOrIgnorePrimaryKeyStatements { + statement.destroy() + } + self.insertOrIgnorePrimaryKeyStatements.removeAll() + for (_, statement) in self.deleteStatements { statement.destroy() } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift new file mode 100644 index 00000000000..68b9ec89cdb --- /dev/null +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -0,0 +1,962 @@ +import Foundation +import SwiftSignalKit +import CryptoUtils + +public struct HashId: Hashable { + public let data: Data + + public init(data: Data) { + precondition(data.count == 16) + self.data = data + } +} + +private func md5Hash(_ data: Data) -> HashId { + let hashData = data.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return HashId(data: hashData) +} + +public final class StorageBox { + public final class Stats { + public final class ContentTypeStats { + public fileprivate(set) var size: Int64 + public fileprivate(set) var messages: [MessageId: Int64] + + init(size: Int64, messages: [MessageId: Int64]) { + self.size = size + self.messages = messages + } + } + + public fileprivate(set) var contentTypes: [UInt8: ContentTypeStats] + + public init(contentTypes: [UInt8: ContentTypeStats]) { + self.contentTypes = contentTypes + } + } + + public final class AllStats { + public fileprivate(set) var total: Stats + public fileprivate(set) var peers: [PeerId: Stats] + + public init(total: Stats, peers: [PeerId: Stats]) { + self.total = total + self.peers = peers + } + } + + public struct Reference { + public var peerId: Int64 + public var messageNamespace: UInt8 + public var messageId: Int32 + + public init(peerId: Int64, messageNamespace: UInt8, messageId: Int32) { + self.peerId = peerId + self.messageNamespace = messageNamespace + self.messageId = messageId + } + } + + public final class Entry { + public let id: Data + public let references: [Reference] + + init(id: Data, references: [Reference]) { + self.id = id + self.references = references + } + } + + public final class Logger { + private let impl: (String) -> Void + + public init(impl: @escaping (String) -> Void) { + self.impl = impl + } + + func log(_ string: @autoclosure () -> String) { + self.impl(string()) + } + } + + private struct ItemInfo { + var id: Data + var contentType: UInt8 + var size: Int64 + + init(id: Data, contentType: UInt8, size: Int64) { + self.id = id + self.contentType = contentType + self.size = size + } + + init(buffer: MemoryBuffer) { + var id = Data() + var contentType: UInt8 = 0 + var size: Int64 = 0 + + withExtendedLifetime(buffer, { + let readBuffer = ReadBuffer(memoryBufferNoCopy: buffer) + var version: UInt8 = 0 + readBuffer.read(&version, offset: 0, length: 1) + let _ = version + + var idLength: UInt16 = 0 + readBuffer.read(&idLength, offset: 0, length: 2) + id.count = Int(idLength) + id.withUnsafeMutableBytes { buffer -> Void in + readBuffer.read(buffer.baseAddress!, offset: 0, length: buffer.count) + } + + readBuffer.read(&contentType, offset: 0, length: 1) + + readBuffer.read(&size, offset: 0, length: 8) + }) + + self.id = id + self.contentType = contentType + self.size = size + } + + func serialize() -> MemoryBuffer { + let writeBuffer = WriteBuffer() + + var version: UInt8 = 0 + writeBuffer.write(&version, length: 1) + + var idLength = UInt16(clamping: self.id.count) + writeBuffer.write(&idLength, length: 2) + self.id.withUnsafeBytes { buffer in + writeBuffer.write(buffer.baseAddress!, length: Int(idLength)) + } + + var contentType = self.contentType + writeBuffer.write(&contentType, length: 1) + + var size = self.size + writeBuffer.write(&size, length: 8) + + return writeBuffer + } + } + + private struct Metadata: Codable { + var version: Int32 + } + + private final class Impl { + let queue: Queue + let logger: StorageBox.Logger + let basePath: String + let valueBox: SqliteValueBox + let hashIdToInfoTable: ValueBoxTable + let idToReferenceTable: ValueBoxTable + let peerIdToIdTable: ValueBoxTable + let peerContentTypeStatsTable: ValueBoxTable + let contentTypeStatsTable: ValueBoxTable + let metadataTable: ValueBoxTable + + init(queue: Queue, logger: StorageBox.Logger, basePath: String) { + self.queue = queue + self.logger = logger + self.basePath = basePath + + let databasePath = self.basePath + "/db" + let _ = try? FileManager.default.createDirectory(atPath: databasePath, withIntermediateDirectories: true) + var valueBox = SqliteValueBox(basePath: databasePath, queue: queue, isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) + if valueBox == nil { + let _ = try? FileManager.default.removeItem(atPath: databasePath) + valueBox = SqliteValueBox(basePath: databasePath, queue: queue, isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) + } + guard let valueBox = valueBox else { + preconditionFailure("Could not open database") + } + self.valueBox = valueBox + + self.hashIdToInfoTable = ValueBoxTable(id: 15, keyType: .binary, compactValuesOnCreation: true) + self.idToReferenceTable = ValueBoxTable(id: 16, keyType: .binary, compactValuesOnCreation: true) + self.peerIdToIdTable = ValueBoxTable(id: 17, keyType: .binary, compactValuesOnCreation: true) + self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true) + self.contentTypeStatsTable = ValueBoxTable(id: 20, keyType: .binary, compactValuesOnCreation: true) + self.metadataTable = ValueBoxTable(id: 21, keyType: .binary, compactValuesOnCreation: true) + + self.performUpdatesIfNeeded() + } + + private func performUpdatesIfNeeded() { + self.valueBox.begin() + + let mainMetadataKey = ValueBoxKey(length: 2) + mainMetadataKey.setUInt8(0, value: 0) + mainMetadataKey.setUInt8(1, value: 0) + + var metadata: Metadata + if let value = self.valueBox.get(self.metadataTable, key: mainMetadataKey), let parsedValue = try? JSONDecoder().decode(Metadata.self, from: value.makeData()) { + metadata = parsedValue + } else { + metadata = Metadata(version: 0) + } + + if metadata.version != 2 { + self.reindexPeerStats() + + metadata.version = 2 + if let data = try? JSONEncoder().encode(metadata) { + self.valueBox.set(self.metadataTable, key: mainMetadataKey, value: MemoryBuffer(data: data)) + } + } + + self.valueBox.commit() + } + + private func reindexPeerStats() { + self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable) + + let mainKey = ValueBoxKey(length: 16) + self.valueBox.scan(self.peerIdToIdTable, keys: { key in + let peerId = key.getInt64(0) + let hashId = key.getData(8, length: 16) + + mainKey.setData(0, value: hashId) + + if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + let info = ItemInfo(buffer: currentInfoValue) + self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: info.size) + } + + return true + }) + } + + func reset() { + self.valueBox.begin() + + self.valueBox.removeAllFromTable(self.hashIdToInfoTable) + self.valueBox.removeAllFromTable(self.idToReferenceTable) + self.valueBox.removeAllFromTable(self.peerIdToIdTable) + self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable) + self.valueBox.removeAllFromTable(self.contentTypeStatsTable) + self.valueBox.removeAllFromTable(self.metadataTable) + + self.valueBox.commit() + } + + private func internalAddSize(contentType: UInt8, delta: Int64) { + let key = ValueBoxKey(length: 1) + key.setUInt8(0, value: contentType) + + var currentSize: Int64 = 0 + if let value = self.valueBox.get(self.contentTypeStatsTable, key: key) { + value.read(¤tSize, offset: 0, length: 8) + } + + currentSize += delta + + if currentSize < 0 { + //assertionFailure() + currentSize = 0 + } + + self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + } + + private func internalAddSize(peerId: Int64, contentType: UInt8, delta: Int64) { + let key = ValueBoxKey(length: 8 + 1) + key.setInt64(0, value: peerId) + key.setUInt8(8, value: contentType) + + var currentSize: Int64 = 0 + if let value = self.valueBox.get(self.peerContentTypeStatsTable, key: key) { + value.read(¤tSize, offset: 0, length: 8) + } + + currentSize += delta + + if currentSize < 0 { + //assertionFailure() + currentSize = 0 + } + + self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + } + + func internalAdd(reference: Reference, to id: Data, contentType: UInt8, size: Int64?) { + let hashId = md5Hash(id) + + let mainKey = ValueBoxKey(length: 16) + mainKey.setData(0, value: hashId.data) + + var previousContentType: UInt8? + var previousSize: Int64 = 0 + if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + var info = ItemInfo(buffer: currentInfoValue) + previousContentType = info.contentType + previousSize = info.size + info.contentType = contentType + if let size = size { + info.size = size + } + self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: info.serialize()) + } else { + self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: ItemInfo(id: id, contentType: contentType, size: size ?? 0).serialize()) + } + + let updatedSize = size ?? previousSize + let deltaSize = updatedSize - previousSize + + if let previousContentType = previousContentType { + if previousContentType != contentType { + var referencingPeers = self.peerIdsReferencing(hashId: hashId) + + if previousSize != 0 { + self.internalAddSize(contentType: previousContentType, delta: -previousSize) + + for peerId in referencingPeers { + self.internalAddSize(peerId: peerId, contentType: previousContentType, delta: -previousSize) + } + } + + if updatedSize != 0 { + self.internalAddSize(contentType: contentType, delta: updatedSize) + + if !referencingPeers.contains(reference.peerId) { + referencingPeers.insert(reference.peerId) + } + for peerId in referencingPeers { + self.internalAddSize(peerId: peerId, contentType: contentType, delta: updatedSize) + } + } + } else if deltaSize != 0 { + self.internalAddSize(contentType: contentType, delta: deltaSize) + + let referencingPeers = self.peerIdsReferencing(hashId: hashId) + + for peerId in referencingPeers { + self.internalAddSize(peerId: peerId, contentType: previousContentType, delta: deltaSize) + } + if !referencingPeers.contains(reference.peerId) { + self.internalAddSize(peerId: reference.peerId, contentType: previousContentType, delta: updatedSize) + } + } + } else if updatedSize != 0 { + self.internalAddSize(contentType: contentType, delta: updatedSize) + + var referencingPeers = self.peerIdsReferencing(hashId: hashId) + if !referencingPeers.contains(reference.peerId) { + referencingPeers.insert(reference.peerId) + } + for peerId in referencingPeers { + self.internalAddSize(peerId: peerId, contentType: contentType, delta: updatedSize) + } + } + + let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4) + idKey.setData(0, value: hashId.data) + idKey.setInt64(hashId.data.count, value: reference.peerId) + idKey.setUInt8(hashId.data.count + 8, value: reference.messageNamespace) + idKey.setInt32(hashId.data.count + 8 + 1, value: reference.messageId) + + var alreadyStored = false + if !self.valueBox.exists(self.idToReferenceTable, key: idKey) { + self.valueBox.setOrIgnore(self.idToReferenceTable, key: idKey, value: MemoryBuffer()) + } else { + alreadyStored = true + } + + if !alreadyStored { + let peerIdIdKey = ValueBoxKey(length: 8 + 16) + peerIdIdKey.setInt64(0, value: reference.peerId) + peerIdIdKey.setData(8, value: hashId.data) + + self.valueBox.setOrIgnore(self.peerIdToIdTable, key: peerIdIdKey, value: MemoryBuffer()) + } + } + + func add(reference: Reference, to id: Data, contentType: UInt8, size: Int64?) { + self.valueBox.begin() + + self.internalAdd(reference: reference, to: id, contentType: contentType, size: size) + + self.valueBox.commit() + } + + func batchAdd(items: [(reference: Reference, id: Data, contentType: UInt8, size: Int64)]) { + self.valueBox.begin() + + for (reference, id, contentType, size) in items { + self.internalAdd(reference: reference, to: id, contentType: contentType, size: size) + } + + self.valueBox.commit() + } + + private func peerIdsReferencing(hashId: HashId) -> Set { + let mainKey = ValueBoxKey(length: 16) + mainKey.setData(0, value: hashId.data) + + var peerIds = Set() + self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in + let peerId = key.getInt64(16) + peerIds.insert(peerId) + return true + }, limit: 0) + + return peerIds + } + + func update(id: Data, size: Int64) { + self.valueBox.begin() + + let hashId = md5Hash(id) + + let mainKey = ValueBoxKey(length: 16) + mainKey.setData(0, value: hashId.data) + + if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + var info = ItemInfo(buffer: currentInfoValue) + + var sizeDelta: Int64 = 0 + if info.size != size { + sizeDelta = size - info.size + info.size = size + + self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: info.serialize()) + } + + if sizeDelta != 0 { + self.internalAddSize(contentType: info.contentType, delta: sizeDelta) + } + + for peerId in self.peerIdsReferencing(hashId: hashId) { + self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta) + } + } + + self.valueBox.commit() + } + + func addEmptyReferencesIfNotReferenced(ids: [(id: Data, size: Int64)], contentType: UInt8) -> Int { + self.valueBox.begin() + + let mainKey = ValueBoxKey(length: 16) + var addedCount = 0 + + for (id, size) in ids { + let hashId = md5Hash(id) + mainKey.setData(0, value: hashId.data) + if self.valueBox.exists(self.hashIdToInfoTable, key: mainKey) { + continue + } + + self.internalAdd(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: id, contentType: contentType, size: size) + addedCount += 1 + } + + self.valueBox.commit() + + return addedCount + } + + private func internalRemove(hashId: Data) { + let mainKey = ValueBoxKey(length: 16) + let peerIdIdKey = ValueBoxKey(length: 8 + 16) + + mainKey.setData(0, value: hashId) + + guard let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) else { + return + } + let info = ItemInfo(buffer: infoValue) + self.valueBox.remove(self.hashIdToInfoTable, key: mainKey, secure: false) + + if info.size != 0 { + self.internalAddSize(contentType: info.contentType, delta: -info.size) + } + + var referenceKeys: [ValueBoxKey] = [] + self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in + referenceKeys.append(key) + return true + }, limit: 0) + var peerIds = Set() + for key in referenceKeys { + peerIds.insert(key.getInt64(16)) + self.valueBox.remove(self.idToReferenceTable, key: key, secure: false) + } + + for peerId in peerIds { + peerIdIdKey.setInt64(0, value: peerId) + peerIdIdKey.setData(8, value: hashId) + + if self.valueBox.exists(self.peerIdToIdTable, key: peerIdIdKey) { + self.valueBox.remove(self.peerIdToIdTable, key: peerIdIdKey, secure: false) + + if info.size != 0 { + self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: -info.size) + } + } + } + } + + func remove(ids: [Data]) { + self.valueBox.begin() + + for id in ids { + self.internalRemove(hashId: md5Hash(id).data) + } + + self.valueBox.commit() + } + + func allPeerIds() -> [PeerId] { + var result: [PeerId] = [] + + self.valueBox.begin() + + var fromKey = ValueBoxKey(length: 8) + fromKey.setInt64(0, value: 0) + + let toKey = ValueBoxKey(length: 8) + toKey.setInt64(0, value: Int64.max) + + while true { + var peerId: Int64? + self.valueBox.range(self.peerIdToIdTable, start: fromKey, end: toKey, keys: { key in + peerId = key.getInt64(0) + return false + }, limit: 1) + + if let peerId = peerId { + if peerId != 0 { + result.append(PeerId(peerId)) + } + + fromKey.setInt64(0, value: peerId) + fromKey = fromKey.successor + } else { + break + } + } + + self.valueBox.commit() + + return result + } + + private func allInternal(peerId: PeerId) -> [Data] { + var hashIds: [Data] = [] + let peerIdIdKey = ValueBoxKey(length: 8) + peerIdIdKey.setInt64(0, value: peerId.toInt64()) + self.valueBox.range(self.peerIdToIdTable, start: peerIdIdKey, end: peerIdIdKey.successor, keys: { key in + hashIds.append(key.getData(8, length: 16)) + return true + }, limit: 0) + + var result: [Data] = [] + let mainKey = ValueBoxKey(length: 16) + for hashId in hashIds { + mainKey.setData(0, value: hashId) + if let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + let info = ItemInfo(buffer: infoValue) + result.append(info.id) + } + } + + return result + } + + func all(peerId: PeerId) -> [Data] { + self.valueBox.begin() + + let result = self.allInternal(peerId: peerId) + + self.valueBox.commit() + + return result + } + + func enumerateItems(startingWith startId: Data?, limit: Int) -> (ids: [Data], nextStartId: Data?) { + self.valueBox.begin() + + let startKey: ValueBoxKey + if let startId = startId, startId.count == 16 { + startKey = ValueBoxKey(length: 16) + startKey.setData(0, value: startId) + } else { + startKey = ValueBoxKey(length: 1) + startKey.setUInt8(0, value: 0) + } + + let endKey = ValueBoxKey(length: 16) + for i in 0 ..< 16 { + endKey.setUInt8(i, value: 0xff) + } + + var ids: [Data] = [] + var nextKey: ValueBoxKey? + self.valueBox.range(self.hashIdToInfoTable, start: startKey, end: endKey, values: { key, value in + nextKey = key + + let info = ItemInfo(buffer: value) + ids.append(info.id) + + return true + }, limit: limit) + + self.valueBox.commit() + + var nextId = nextKey?.getData(0, length: 16) + if nextId == startId { + nextId = nil + } + + return (ids, nextId) + } + + func all() -> [Entry] { + var result: [Entry] = [] + + self.valueBox.begin() + + var currentId: Data? + var currentReferences: [Reference] = [] + + let mainKey = ValueBoxKey(length: 16) + + self.valueBox.scan(self.idToReferenceTable, keys: { key in + let id = key.getData(0, length: 16) + + let peerId = key.getInt64(16) + let messageNamespace: UInt8 = key.getUInt8(16 + 8) + let messageId = key.getInt32(16 + 8 + 1) + + let reference = Reference(peerId: peerId, messageNamespace: messageNamespace, messageId: messageId) + + if currentId == id { + currentReferences.append(reference) + } else { + if let currentId = currentId, !currentReferences.isEmpty { + mainKey.setData(0, value: currentId) + if let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + let info = ItemInfo(buffer: infoValue) + result.append(StorageBox.Entry(id: info.id, references: currentReferences)) + } + currentReferences.removeAll(keepingCapacity: true) + } + currentId = id + currentReferences.append(reference) + } + + return true + }) + + self.valueBox.commit() + + return result + } + + func get(ids: [Data]) -> [Entry] { + var result: [Entry] = [] + + self.valueBox.begin() + + let idKey = ValueBoxKey(length: 16) + + for id in ids { + let hashId = md5Hash(id) + idKey.setData(0, value: hashId.data) + var currentReferences: [Reference] = [] + self.valueBox.range(self.idToReferenceTable, start: idKey, end: idKey.successor, keys: { key in + let peerId = key.getInt64(16) + let messageNamespace: UInt8 = key.getUInt8(16 + 8) + let messageId = key.getInt32(16 + 8 + 1) + + let reference = Reference(peerId: peerId, messageNamespace: messageNamespace, messageId: messageId) + + currentReferences.append(reference) + return true + }, limit: 0) + + if !currentReferences.isEmpty { + result.append(StorageBox.Entry(id: id, references: currentReferences)) + } + } + + self.valueBox.commit() + + return result + } + + func getAllStats() -> AllStats { + self.valueBox.begin() + + let allStats = AllStats(total: StorageBox.Stats(contentTypes: [:]), peers: [:]) + + self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in + var size: Int64 = 0 + value.read(&size, offset: 0, length: 8) + allStats.total.contentTypes[key.getUInt8(0)] = Stats.ContentTypeStats(size: size, messages: [:]) + + return true + }) + + self.valueBox.scan(self.peerContentTypeStatsTable, values: { key, value in + var size: Int64 = 0 + value.read(&size, offset: 0, length: 8) + + let peerId = key.getInt64(0) + let contentType = key.getUInt8(8) + if allStats.peers[PeerId(peerId)] == nil { + allStats.peers[PeerId(peerId)] = StorageBox.Stats(contentTypes: [:]) + } + allStats.peers[PeerId(peerId)]?.contentTypes[contentType] = Stats.ContentTypeStats(size: size, messages: [:]) + + return true + }) + + let idKey = ValueBoxKey(length: 16 + 8) + + let mainKey = ValueBoxKey(length: 16) + self.valueBox.scan(self.peerIdToIdTable, keys: { key in + let peerId = key.getInt64(0) + if peerId == 0 { + return true + } + + let hashId = key.getData(8, length: 16) + + mainKey.setData(0, value: hashId) + if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + let info = ItemInfo(buffer: currentInfoValue) + if info.size != 0 { + idKey.setData(0, value: hashId) + idKey.setInt64(16, value: peerId) + + let contentType = info.contentType + if contentType == 0 { + return true + } + + self.valueBox.range(self.idToReferenceTable, start: idKey, end: idKey.successor, keys: { subKey in + let messageNamespace: UInt8 = subKey.getUInt8(16 + 8) + let messageId = subKey.getInt32(16 + 8 + 1) + + if messageId != 0 { + allStats.total.contentTypes[contentType]?.messages[MessageId(peerId: PeerId(peerId), namespace: Int32(messageNamespace), id: messageId), default: 0] += info.size + allStats.peers[PeerId(peerId)]?.contentTypes[contentType]?.messages[MessageId(peerId: PeerId(peerId), namespace: Int32(messageNamespace), id: messageId), default: 0] += info.size + } + + return true + }, limit: 0) + } + } + + return true + }) + + self.valueBox.commit() + + return allStats + } + + func remove(peerId: Int64?, contentTypes: [UInt8]) -> [Data] { + var resultIds: [Data] = [] + + self.valueBox.begin() + + var scannedIds: [Data: Data] = [:] + + for contentType in contentTypes { + self.internalAddSize(contentType: contentType, delta: 0) + } + + self.valueBox.scan(self.hashIdToInfoTable, values: { key, value in + let info = ItemInfo(buffer: value) + if !contentTypes.contains(info.contentType) { + return true + } + scannedIds[key.getData(0, length: 16)] = info.id + return true + }) + + if let peerId = peerId { + var filteredHashIds: [Data] = [] + self.valueBox.scan(self.idToReferenceTable, keys: { key in + let id = key.getData(0, length: 16) + if scannedIds[id] == nil { + return true + } + + let itemPeerId = key.getInt64(16) + //let messageNamespace: UInt8 = key.getUInt8(16 + 8) + //let messageId = key.getInt32(16 + 8 + 1) + + if itemPeerId == peerId { + filteredHashIds.append(id) + } + + return true + }) + for hashId in filteredHashIds { + if let id = scannedIds[hashId] { + self.internalRemove(hashId: hashId) + resultIds.append(id) + } + } + } else { + for (hashId, id) in scannedIds { + self.internalRemove(hashId: hashId) + resultIds.append(id) + } + } + + if let peerId = peerId { + let _ = peerId + } else { + + } + + self.valueBox.commit() + + return Array(resultIds) + } + + func remove(peerIds: Set) -> [Data] { + var resultIds: [Data] = [] + + self.valueBox.begin() + + var scannedIds = Set() + for peerId in peerIds { + scannedIds.formUnion(self.allInternal(peerId: peerId)) + } + + for id in scannedIds { + self.internalRemove(hashId: md5Hash(id).data) + resultIds.append(id) + } + + self.valueBox.commit() + + return Array(resultIds) + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + public init(queue: Queue = Queue(name: "StorageBox"), logger: StorageBox.Logger, basePath: String) { + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, logger: logger, basePath: basePath) + }) + } + + public func add(reference: Reference, to id: Data, contentType: UInt8) { + self.impl.with { impl in + impl.add(reference: reference, to: id, contentType: contentType, size: nil) + } + } + + public func update(id: Data, size: Int64) { + self.impl.with { impl in + impl.update(id: id, size: size) + } + } + + public func addEmptyReferencesIfNotReferenced(ids: [(id: Data, size: Int64)], contentType: UInt8, completion: @escaping (Int) -> Void) { + self.impl.with { impl in + let addedCount = impl.addEmptyReferencesIfNotReferenced(ids: ids, contentType: contentType) + + completion(addedCount) + } + } + + public func batchAdd(items: [(reference: Reference, id: Data, contentType: UInt8, size: Int64)]) { + self.impl.with { impl in + impl.batchAdd(items: items) + } + } + + public func remove(ids: [Data]) { + self.impl.with { impl in + impl.remove(ids: ids) + } + } + + public func all() -> Signal<[Entry], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.all()) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func allPeerIds() -> Signal<[PeerId], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.allPeerIds()) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func all(peerId: PeerId) -> Signal<[Data], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.all(peerId: peerId)) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func get(ids: [Data]) -> Signal<[Entry], NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.get(ids: ids)) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func getAllStats() -> Signal { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.getAllStats()) + subscriber.putCompletion() + + return EmptyDisposable + } + } + + public func remove(peerId: PeerId?, contentTypes: [UInt8], completion: @escaping ([Data]) -> Void) { + self.impl.with { impl in + let ids = impl.remove(peerId: peerId?.toInt64(), contentTypes: contentTypes) + completion(ids) + } + } + + public func remove(peerIds: Set, completion: @escaping ([Data]) -> Void) { + self.impl.with { impl in + let ids = impl.remove(peerIds: peerIds) + completion(ids) + } + } + + public func reset() { + self.impl.with { impl in + impl.reset() + } + } + + public func enumerateItems(startingWith startId: Data?, limit: Int) -> Signal<(ids: [Data], nextStartId: Data?), NoError> { + return self.impl.signalWith { impl, subscriber in + subscriber.putNext(impl.enumerateItems(startingWith: startId, limit: limit)) + subscriber.putCompletion() + + return EmptyDisposable + } + } +} diff --git a/submodules/Postbox/Sources/TimeBasedCleanup.swift b/submodules/Postbox/Sources/TimeBasedCleanup.swift index 685dbb6e097..0436501db89 100644 --- a/submodules/Postbox/Sources/TimeBasedCleanup.swift +++ b/submodules/Postbox/Sources/TimeBasedCleanup.swift @@ -39,32 +39,6 @@ public func printOpenFiles() { } } -/* - +(void) lsof - { - int flags; - int fd; - char buf[MAXPATHLEN+1] ; - int n = 1 ; - - for (fd = 0; fd < (int) FD_SETSIZE; fd++) { - errno = 0; - flags = fcntl(fd, F_GETFD, 0); - if (flags == -1 && errno) { - if (errno != EBADF) { - return ; - } - else - continue; - } - fcntl(fd , F_GETPATH, buf ) ; - NSLog( @"File Descriptor %d number %d in use for: %s",fd,n , buf ) ; - ++n ; - } - } - - */ - private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: inout [InodeInfo]) -> ScanFilesResult { var result = ScanFilesResult() @@ -113,7 +87,7 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: i return result } -private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64) { +private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) { var removedSize: UInt64 = 0 inodes.sort(by: { lhs, rhs in @@ -139,7 +113,10 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI free(pathBuffer) } + var unlinkedResourceIds: [Data] = [] + for path in paths { + let isMainPath = path == mainStoragePath if let dp = opendir(path) { while true { guard let dirp = readdir(dp) else { @@ -162,6 +139,17 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI var value = stat() if stat(pathBuffer, &value) == 0 { if inodesToDelete.contains(value.st_ino) { + if isMainPath { + let nameLength = strnlen(&dirp.pointee.d_name.0, 1024) + let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none) + withExtendedLifetime(nameData, { + if let fileName = String(data: nameData, encoding: .utf8) { + if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) { + unlinkedResourceIds.append(idData) + } + } + }) + } unlink(pathBuffer) } } @@ -169,11 +157,17 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI closedir(dp) } } + + if !unlinkedResourceIds.isEmpty { + storageBox.remove(ids: unlinkedResourceIds) + } } private final class TimeBasedCleanupImpl { private let queue: Queue + private let storageBox: StorageBox private let generalPaths: [String] + private let totalSizeBasedPath: String private let shortLivedPaths: [String] private var scheduledTouches: [String] = [] @@ -197,9 +191,11 @@ private final class TimeBasedCleanupImpl { } } - init(queue: Queue, generalPaths: [String], shortLivedPaths: [String]) { + init(queue: Queue, storageBox: StorageBox, generalPaths: [String], totalSizeBasedPath: String, shortLivedPaths: [String]) { self.queue = queue + self.storageBox = storageBox self.generalPaths = generalPaths + self.totalSizeBasedPath = totalSizeBasedPath self.shortLivedPaths = shortLivedPaths } @@ -220,7 +216,9 @@ private final class TimeBasedCleanupImpl { private func resetScan(general: Int32, shortLived: Int32, gigabytesLimit: Int32) { let generalPaths = self.generalPaths + let totalSizeBasedPath = self.totalSizeBasedPath let shortLivedPaths = self.shortLivedPaths + let storageBox = self.storageBox let scanOnce = Signal { subscriber in DispatchQueue.global(qos: .background).async { var removedShortLivedCount: Int = 0 @@ -233,7 +231,12 @@ private final class TimeBasedCleanupImpl { var paths: [String] = [] let timestamp = Int32(Date().timeIntervalSince1970) + + /*#if DEBUG + let bytesLimit: UInt64 = 10 * 1024 * 1024 + #else*/ let bytesLimit = UInt64(gigabytesLimit) * 1024 * 1024 * 1024 + //#endif let oldestShortLivedTimestamp = timestamp - shortLived let oldestGeneralTimestamp = timestamp - general @@ -255,15 +258,19 @@ private final class TimeBasedCleanupImpl { removedGeneralCount += scanResult.unlinkedCount totalLimitSize += scanResult.totalSize } + do { + let scanResult = scanFiles(at: totalSizeBasedPath, olderThan: 0, inodes: &inodes) + if !paths.contains(totalSizeBasedPath) { + paths.append(totalSizeBasedPath) + } + removedGeneralCount += scanResult.unlinkedCount + totalLimitSize += scanResult.totalSize + } if totalLimitSize > bytesLimit { - mapFiles(paths: paths, inodes: &inodes, removeSize: totalLimitSize - bytesLimit) + mapFiles(paths: paths, inodes: &inodes, removeSize: totalLimitSize - bytesLimit, mainStoragePath: totalSizeBasedPath, storageBox: storageBox) } - #if DEBUG - //printOpenFiles() - #endif - if removedShortLivedCount != 0 || removedGeneralCount != 0 || removedGeneralLimitCount != 0 { postboxLog("[TimeBasedCleanup] \(CFAbsoluteTimeGetCurrent() - startTime) s removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files") } @@ -318,10 +325,10 @@ final class TimeBasedCleanup { private let queue = Queue() private let impl: QueueLocalObject - init(generalPaths: [String], shortLivedPaths: [String]) { + init(storageBox: StorageBox, generalPaths: [String], totalSizeBasedPath: String, shortLivedPaths: [String]) { let queue = self.queue self.impl = QueueLocalObject(queue: self.queue, generate: { - return TimeBasedCleanupImpl(queue: queue, generalPaths: generalPaths, shortLivedPaths: shortLivedPaths) + return TimeBasedCleanupImpl(queue: queue, storageBox: storageBox, generalPaths: generalPaths, totalSizeBasedPath: totalSizeBasedPath, shortLivedPaths: shortLivedPaths) }) } diff --git a/submodules/Postbox/Sources/ValueBoxKey.swift b/submodules/Postbox/Sources/ValueBoxKey.swift index e73b21ddf74..f192bde0f61 100644 --- a/submodules/Postbox/Sources/ValueBoxKey.swift +++ b/submodules/Postbox/Sources/ValueBoxKey.swift @@ -83,6 +83,15 @@ public struct ValueBoxKey: Equatable, Hashable, CustomStringConvertible, Compara memcpy(self.memory + offset, &varValue, 2) } + public func getData(_ offset: Int, length: Int) -> Data { + assert(offset >= 0 && offset + length <= self.length) + var value = Data(count: length) + let _ = value.withUnsafeMutableBytes { bytes in + memcpy(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), self.memory + offset, length) + } + return value + } + public func getInt32(_ offset: Int) -> Int32 { assert(offset >= 0 && offset + 4 <= self.length) var value: Int32 = 0 diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 5ffa1162f29..67d014e5527 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -165,6 +165,7 @@ private final class PhoneView: UIView { let videoContent = NativeVideoContent( id: .message(1, MediaId(namespace: 0, id: Int64.random(in: 0.. Void)? - let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "–").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium) + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium) controller.action = { [weak state] in dismissImpl?() if state?.isPremium == false { @@ -1628,23 +1628,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } updateIsFocused(true) -// let controller = PremiumDemoScreen( -// context: accountContext, -// subject: demoSubject, -// source: .intro(state?.price), -// order: state?.configuration.perks, -// action: { -// if state?.isPremium == false { -// buy() -// } -// } -// ) -// controller.disposed = { -// updateIsFocused(false) -// } -// present(controller) -// updateIsFocused(true) - addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) } )) @@ -2046,7 +2029,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { strongSelf.selectedProductId = strongSelf.products?.last?.id for (_, video) in promoConfiguration.videos { - strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) } } @@ -2323,7 +2306,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var packReference: StickerPackReference? if let file = file { for attribute in file.attributes { - if case let .CustomEmoji(_, _, reference) = attribute { + if case let .CustomEmoji(_, _, _, reference) = attribute { packReference = reference } } @@ -2395,7 +2378,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { tapAction: { [weak state, weak environment] _, _ in if let emojiFile = state?.emojiFile, let controller = environment?.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { let controller = accountContext.sharedContext.makeStickerPackScreen(context: accountContext, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: loadedEmojiPack.flatMap { [$0] } ?? [], parentNavigationController: navigationController, sendSticker: { _, _, _ in return false }) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index fadd9ff1847..5e9910a2601 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1003,7 +1003,7 @@ private final class LimitSheetComponent: CombinedComponent { }) } )), - backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: animateOut ), environment: { @@ -1011,6 +1011,8 @@ private final class LimitSheetComponent: CombinedComponent { SheetComponentEnvironment( isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: nil, dismiss: { animated in if animated { animateOut.invoke(Action { _ in diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 70155ed74ef..676672d5ef8 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -628,7 +628,6 @@ public class PremiumLimitsListScreen: ViewController { self.wrappingView = UIView() self.containerView = UIView() -// self.scrollView = UIScrollView() self.backgroundView = ComponentHostView() self.pagerView = ComponentHostView() self.closeView = ComponentHostView() @@ -636,10 +635,7 @@ public class PremiumLimitsListScreen: ViewController { self.footerNode = FooterNode(theme: self.presentationData.theme, title: buttonTitle, gloss: gloss) super.init() - -// self.scrollView.delegate = self -// self.scrollView.showsVerticalScrollIndicator = false - + self.containerView.clipsToBounds = true self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor @@ -651,8 +647,7 @@ public class PremiumLimitsListScreen: ViewController { self.containerView.addSubview(self.pagerView) self.containerView.addSubnode(self.footerNode) self.containerView.addSubview(self.closeView) -// self.scrollView.addSubview(self.hostView) - + self.footerNode.action = { [weak self] in self?.controller?.action() } @@ -889,7 +884,7 @@ public class PremiumLimitsListScreen: ViewController { } else { self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) self.containerView.layer.cornerRadius = 10.0 - + let verticalInset: CGFloat = 44.0 let maxSide = max(layout.size.width, layout.size.height) @@ -899,7 +894,6 @@ public class PremiumLimitsListScreen: ViewController { } transition.setFrame(view: self.containerView, frame: clipFrame) -// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) var clipLayout = layout.withUpdatedSize(clipFrame.size) if case .regular = layout.metrics.widthClass { @@ -914,10 +908,12 @@ public class PremiumLimitsListScreen: ViewController { } func updated(transition: Transition) { - guard let controller = self.controller, let layout = self.currentLayout else { + guard let controller = self.controller else { return } + let contentSize = self.containerView.bounds.size + let backgroundSize = self.backgroundView.update( transition: .immediate, component: AnyComponent( @@ -929,9 +925,9 @@ public class PremiumLimitsListScreen: ViewController { ]) ), environment: {}, - containerSize: CGSize(width: layout.size.width, height: layout.size.width) + containerSize: CGSize(width: contentSize.width, height: contentSize.width) ) - self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) + self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) var isStandalone = false if case .other = controller.source { @@ -1215,9 +1211,9 @@ public class PremiumLimitsListScreen: ViewController { ) ), environment: {}, - containerSize: CGSize(width: layout.size.width, height: self.containerView.frame.height) + containerSize: contentSize ) - self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize) + self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize) } } @@ -1228,7 +1224,6 @@ public class PremiumLimitsListScreen: ViewController { closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))! self.cachedCloseImage = closeImage } - let closeSize = self.closeView.update( transition: .immediate, @@ -1259,7 +1254,7 @@ public class PremiumLimitsListScreen: ViewController { environment: {}, containerSize: CGSize(width: 30.0, height: 30.0) ) - self.closeView.frame = CGRect(origin: CGPoint(x: layout.size.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize) + self.closeView.frame = CGRect(origin: CGPoint(x: contentSize.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize) } private var cachedCloseImage: UIImage? diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 63c5935914e..14b5821d7bb 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -115,12 +115,12 @@ private class StickerNode: ASDisplayNode { let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width * 1.6), height: Int(fittedDimensions.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: fittedDimensions)) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions)) - self.disposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: file.resource).start()) + self.disposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) if let effect = file.videoThumbnails.first { - self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: effect.resource).start()) + self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: effect.resource).start()) let source = AnimatedStickerResourceSource(account: self.context.account, resource: effect.resource, fitzModifier: nil) diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index dd6715a40e3..04597be426e 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -82,6 +82,7 @@ public final class QrCodeScanScreen: ViewController { public enum Subject { case authTransfer(activeSessionsContext: ActiveSessionsContext) case peer + case custom(info: String) } private let context: AccountContext @@ -97,9 +98,12 @@ public final class QrCodeScanScreen: ViewController { } public var showMyCode: () -> Void = {} + public var completion: (String?) -> Void = { _ in } private var codeResolved = false + private var validLayout: ContainerViewLayout? + public init(context: AccountContext, subject: QrCodeScanScreen.Subject) { self.context = context self.subject = subject @@ -126,7 +130,9 @@ public final class QrCodeScanScreen: ViewController { (strongSelf.displayNode as! QrCodeScanScreenNode).updateInForeground(inForeground) }) - if case .peer = subject { + if case .custom = subject { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + } else if case .peer = subject { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Contacts_QrCode_MyCode, style: .plain, target: self, action: #selector(self.myCodePressed)) } else { #if DEBUG @@ -145,6 +151,11 @@ public final class QrCodeScanScreen: ViewController { self.approveDisposable.dispose() } + @objc private func cancelPressed() { + self.completion(nil) + self.dismissAnimated() + } + @objc private func myCodePressed() { self.showMyCode() } @@ -153,6 +164,16 @@ public final class QrCodeScanScreen: ViewController { self.dismissWithSession(session: nil) } + private var animatedIn = false + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if case .custom = self.subject, !self.animatedIn, let layout = self.validLayout { + self.animatedIn = true + self.controllerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + private func dismissWithSession(session: RecentAccountSession?) { guard case let .authTransfer(activeSessionsContext) = self.subject else { return @@ -184,6 +205,22 @@ public final class QrCodeScanScreen: ViewController { } } + public func dismissAnimated() { + guard let layout = self.validLayout else { + return + } + self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in + self.dismiss() + }) + } + + private func completeWithCode(_ code: String) { + guard case .custom = self.subject else { + return + } + self.completion(code) + } + override public func loadDisplayNode() { self.displayNode = QrCodeScanScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject) @@ -235,6 +272,8 @@ public final class QrCodeScanScreen: ViewController { } }) } + case .custom: + strongSelf.completeWithCode(code) } }) @@ -246,6 +285,8 @@ public final class QrCodeScanScreen: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + self.validLayout = layout + (self.displayNode as! QrCodeScanScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } @@ -344,6 +385,9 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie case .peer: title = "" text = "" + case let .custom(info): + title = presentationData.strings.AuthSessions_AddDevice_ScanTitle + text = info } self.titleNode = ImmediateTextNode() @@ -445,6 +489,8 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie filteredCodes = codes.filter { $0.message.hasPrefix("tg://") } case .peer: filteredCodes = codes.filter { $0.message.hasPrefix("https://t.me/") || $0.message.hasPrefix("t.me/") } + case .custom: + filteredCodes = codes } if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) { if strongSelf.codeWithError != code.message { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 482556ec013..78726fe60f8 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -277,7 +277,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return .single(nil) } return Signal { subscriber in - let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start() let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource) |> filter(\.complete) |> take(1)).start(next: { data in @@ -425,7 +425,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { for item in featuredEmojiPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if filterList.contains(alt) { filteredFiles.append(item.file) } @@ -1395,7 +1395,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -1424,7 +1424,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -1458,6 +1458,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { })) } }, + updateScrollingToItemGroup: { + }, chatPeerId: nil, peekBehavior: nil, customLayout: emojiContentLayout, @@ -1465,7 +1467,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView ), externalExpansionView: self.view, - useOpaqueTheme: false + useOpaqueTheme: false, + hideBackground: false ) } @@ -1742,7 +1745,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if additionalAnimation == nil && itemNode.item.isCustom { outer: for attribute in itemNode.item.stillAnimation.attributes { - if case let .CustomEmoji(_, alt, _) = attribute { + if case let .CustomEmoji(_, _, alt, _) = attribute { if let availableReactions = self.availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == .builtin(alt) { @@ -1807,6 +1810,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { for animationLayer in allLayers { let baseItemLayer = InlineStickerItemLayer( context: itemNode.context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), file: itemNode.item.listAnimation, @@ -2418,6 +2422,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { for animationLayer in allLayers { let baseItemLayer = InlineStickerItemLayer( context: itemNode.context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemNode.item.listAnimation.fileId.id, file: itemNode.item.listAnimation), file: itemNode.item.listAnimation, diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 66ef9faa125..e9b261aa050 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -135,11 +135,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { strongSelf.animateInAnimationNode = nil } - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.appearAnimation.resource)).start() - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.stillAnimation.resource)).start() - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.listAnimation.resource)).start() + self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.appearAnimation.resource)).start() + self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.stillAnimation.resource)).start() + self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.listAnimation.resource)).start() if let applicationAnimation = item.applicationAnimation { - self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: applicationAnimation.resource)).start() + self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: applicationAnimation.resource)).start() } } diff --git a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift index d059463f7a8..39636a9e706 100644 --- a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +++ b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift @@ -15,15 +15,21 @@ public enum FetchMediaDataState { case data(MediaResourceData) } -public func fetchMediaData(context: AccountContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal<(FetchMediaDataState, Bool), NoError> { +public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, forceVideo: Bool = false) -> Signal<(FetchMediaDataState, Bool), NoError> { var resource: MediaResource? var isImage = true var fileExtension: String? + var userContentType: MediaResourceUserContentType = .other if let image = mediaReference.media as? TelegramMediaImage { - if let representation = largestImageRepresentation(image.representations) { + userContentType = .image + if let video = image.videoRepresentations.first, forceVideo { + resource = video.resource + isImage = false + } else if let representation = largestImageRepresentation(image.representations) { resource = representation.resource } } else if let file = mediaReference.media as? TelegramMediaFile { + userContentType = MediaResourceUserContentType(file: file) resource = file.resource if file.isVideo || file.mimeType.hasPrefix("video/") { isImage = false @@ -47,7 +53,7 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, mediaRefer if let resource = resource { let fetchedData: Signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: mediaReference.resourceReference(resource)).start() + let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start() let status = postbox.mediaBox.resourceStatus(resource).start(next: { status in switch status { case .Local: @@ -80,8 +86,8 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, mediaRefer } } -public func saveToCameraRoll(context: AccountContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal { - return fetchMediaData(context: context, postbox: postbox, mediaReference: mediaReference) +public func saveToCameraRoll(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) -> Signal { + return fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, mediaReference: mediaReference) |> mapToSignal { state, isImage -> Signal in switch state { case let .progress(value): @@ -134,8 +140,8 @@ public func saveToCameraRoll(context: AccountContext, postbox: Postbox, mediaRef } } -public func copyToPasteboard(context: AccountContext, postbox: Postbox, mediaReference: AnyMediaReference) -> Signal { - return fetchMediaData(context: context, postbox: postbox, mediaReference: mediaReference) +public func copyToPasteboard(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) -> Signal { + return fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, mediaReference: mediaReference) |> mapToSignal { state, isImage -> Signal in if case let .data(data) = state, data.complete { return Signal { subscriber in diff --git a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift index c5b9d906c6e..bf421714252 100644 --- a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift +++ b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift @@ -53,8 +53,9 @@ public extension SegmentedControlTheme { } } -private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in +private func generateSelectionImage(theme: SegmentedControlTheme, cornerRadius: CGFloat) -> UIImage? { + let cornerRadius = cornerRadius - 1.0 + return generateImage(CGSize(width: 4.0 + cornerRadius * 2.0, height: 4.0 + cornerRadius * 2.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -62,8 +63,8 @@ private func generateSelectionImage(theme: SegmentedControlTheme) -> UIImage? { context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 6.0, color: theme.shadowColor.withAlphaComponent(0.12).cgColor) } context.setFillColor(theme.foregroundColor.cgColor) - context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: 16.0, height: 16.0)) - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: cornerRadius * 2.0, height: cornerRadius * 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(2 + cornerRadius), topCapHeight: Int(2 + cornerRadius)) } public struct SegmentedControlItem: Equatable { @@ -75,6 +76,11 @@ public struct SegmentedControlItem: Equatable { } private class SegmentedControlItemNode: HighlightTrackingButtonNode { + override func didLoad() { + super.didLoad() + + self.view.isExclusiveTouch = true + } } public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDelegate { @@ -119,7 +125,12 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg let dividersCount = self._items.count > 2 ? self._items.count - 1 : 0 if self.dividerNodes.count != dividersCount { self.dividerNodes.forEach { $0.removeFromSupernode() } - self.dividerNodes = (0 ..< dividersCount).map { _ in ASDisplayNode() } + self.dividerNodes = (0 ..< dividersCount).map { _ in + let node = ASDisplayNode() + node.backgroundColor = self.theme.dividerColor + return node + } + self.dividerNodes.forEach(self.addSubnode(_:)) } if let layout = self.validLayout { @@ -164,7 +175,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg f(true) } - public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int) { + public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int, cornerRadius: CGFloat = 9.0) { self.theme = theme self._items = items self._selectedIndex = selectedIndex @@ -196,7 +207,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg super.init() self.clipsToBounds = true - self.cornerRadius = 9.0 + self.cornerRadius = cornerRadius self.addSubnode(self.selectionNode) self.itemNodes.forEach(self.addSubnode(_:)) @@ -204,7 +215,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg self.dividerNodes.forEach(self.addSubnode(_:)) self.backgroundColor = self.theme.backgroundColor - self.selectionNode.image = generateSelectionImage(theme: self.theme) + self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: cornerRadius) } override public func didLoad() { @@ -286,6 +297,26 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg } } + public func animateSelection(to point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect { + self.isUserInteractionEnabled = false + self.alpha = 0.0 + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + let selectionFrame = self.selectionNode.frame + transition.animateFrame(node: self.selectionNode, from: self.selectionNode.frame, to: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height))) + return selectionFrame + } + + public func animateSelection(from point: CGPoint, transition: ContainedViewLayoutTransition) -> CGRect { + self.isUserInteractionEnabled = true + self.alpha = 1.0 + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + let selectionFrame = self.selectionNode.frame + transition.animateFrame(node: self.selectionNode, from: CGRect(origin: CGPoint(x: point.x - self.selectionNode.frame.height / 2.0, y: self.selectionNode.frame.minY), size: CGSize(width: self.selectionNode.frame.height, height: self.selectionNode.frame.height)), to: self.selectionNode.frame) + return selectionFrame + } + public func updateTheme(_ theme: SegmentedControlTheme) { guard theme != self.theme else { return @@ -293,7 +324,7 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg self.theme = theme self.backgroundColor = self.theme.backgroundColor - self.selectionNode.image = generateSelectionImage(theme: self.theme) + self.selectionNode.image = generateSelectionImage(theme: self.theme, cornerRadius: self.cornerRadius) for itemNode in self.itemNodes { if let title = itemNode.attributedTitle(for: .normal)?.string { diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 521cf186853..6974707066a 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -110,6 +110,8 @@ swift_library( "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/TelegramUI/Components/NotificationPeerExceptionController", "//submodules/TelegramUI/Components/ChatTimerScreen", + "//submodules/AnimatedAvatarSetNode", + "//submodules/TelegramUI/Components/StorageUsageScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index 93671bb8e06..58f145fd421 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -47,7 +47,7 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal ViewControll return controller } - -//public final class ChangePhoneNumberController: ViewController, MFMailComposeViewControllerDelegate { -// private var controllerNode: ChangePhoneNumberControllerNode { -// return self.displayNode as! ChangePhoneNumberControllerNode -// } -// -// private let context: AccountContext -// -// private var currentData: (Int32, String?, String)? -// private let requestDisposable = MetaDisposable() -// -// var inProgress: Bool = false { -// didSet { -// if self.inProgress { -// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) -// self.navigationItem.rightBarButtonItem = item -// } else { -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) -// } -// self.controllerNode.inProgress = self.inProgress -// } -// } -// var loginWithNumber: ((String) -> Void)? -// -// private let hapticFeedback = HapticFeedback() -// -// private var presentationData: PresentationData -// -// public init(context: AccountContext) { -// self.context = context -// self.presentationData = context.sharedContext.currentPresentationData.with { $0 } -// -// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) -// -// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) -// self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style -// -// self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) -// self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) -// } -// -// required init(coder aDecoder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// deinit { -// self.requestDisposable.dispose() -// } -// -// func updateData(countryCode: Int32, countryName: String, number: String) { -// if self.currentData == nil || self.currentData! != (countryCode, countryName, number) { -// self.currentData = (countryCode, countryName, number) -// if self.isNodeLoaded { -// self.controllerNode.codeAndNumber = (countryCode, countryName, number) -// } -// } -// } -// -// override public func loadDisplayNode() { -// self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData) -// self.displayNodeDidLoad() -// self.controllerNode.selectCountryCode = { [weak self] in -// if let strongSelf = self { -// let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme) -// controller.completeWithCountryCode = { code, name in -// if let strongSelf = self { -// strongSelf.updateData(countryCode: Int32(code), countryName: name, number: strongSelf.controllerNode.codeAndNumber.2) -// strongSelf.controllerNode.activateInput() -// } -// } -// strongSelf.controllerNode.view.endEditing(true) -// strongSelf.push(controller) -// } -// } -// -// loadServerCountryCodes(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine, completion: { [weak self] in -// if let strongSelf = self { -// strongSelf.controllerNode.updateCountryCode() -// } -// }) -// -// self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) -// } -// -// override public func viewWillAppear(_ animated: Bool) { -// super.viewWillAppear(animated) -// -// self.controllerNode.activateInput() -// } -// -// override public func viewDidAppear(_ animated: Bool) { -// super.viewDidAppear(animated) -// -// self.controllerNode.activateInput() -// } -// -// override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { -// super.containerLayoutUpdated(layout, transition: transition) -// -// self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) -// } -// -// @objc func nextPressed() { -// let (code, _, number) = self.controllerNode.codeAndNumber -// var phoneNumber = number -// if let code = code { -// phoneNumber = "\(code)\(phoneNumber)" -// } -// if !number.isEmpty { -// self.inProgress = true -// self.requestDisposable.set((self.context.engine.accountData.requestChangeAccountPhoneNumberVerification(phoneNumber: self.controllerNode.currentNumber) |> deliverOnMainQueue).start(next: { [weak self] next in -// if let strongSelf = self { -// strongSelf.inProgress = false -// (strongSelf.navigationController as? NavigationController)?.pushViewController(changePhoneNumberCodeController(context: strongSelf.context, phoneNumber: strongSelf.controllerNode.currentNumber, codeData: next)) -// } -// }, error: { [weak self] error in -// if let strongSelf = self { -// strongSelf.inProgress = false -// -// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } -// -// let text: String -// var actions: [TextAlertAction] = [] -// switch error { -// case .limitExceeded: -// text = presentationData.strings.Login_CodeFloodError -// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) -// case .invalidPhoneNumber: -// text = presentationData.strings.Login_InvalidPhoneError -// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) -// case .phoneNumberOccupied: -// text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(context: strongSelf.context, number: phoneNumber)).string -// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) -// case .phoneBanned: -// text = presentationData.strings.Login_PhoneBannedError -// actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) -// actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in -// guard let strongSelf = self else { -// return -// } -// let formattedNumber = formatPhoneNumber(context: strongSelf.context, number: number) -// let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" -// let systemVersion = UIDevice.current.systemVersion -// let locale = Locale.current.identifier -// let carrier = CTCarrier() -// let mnc = carrier.mobileNetworkCode ?? "none" -// -// strongSelf.presentEmailComposeController(address: "login@stel.com", subject: presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string) -// })) -// case .generic: -// text = presentationData.strings.Login_UnknownError -// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})) -// } -// -// strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root)) -// } -// })) -// } else { -// self.hapticFeedback.error() -// self.controllerNode.animateError() -// } -// } -// -// private func presentEmailComposeController(address: String, subject: String, body: String) { -// if MFMailComposeViewController.canSendMail() { -// let composeController = MFMailComposeViewController() -// composeController.setToRecipients([address]) -// composeController.setSubject(subject) -// composeController.setMessageBody(body, isHTML: false) -// composeController.mailComposeDelegate = self -// -// self.view.window?.rootViewController?.present(composeController, animated: true, completion: nil) -// } else { -// self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) -// } -// } -// -// public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { -// controller.dismiss(animated: true, completion: nil) -// } -//} diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index cfc2dc7471a..c6228fbbd4d 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -12,6 +12,7 @@ import PresentationDataUtils import AccountContext import OpenInExternalAppUI import ItemListPeerActionItem +import StorageUsageScreen private final class DataAndStorageControllerArguments { let openStorageUsage: () -> Void @@ -541,8 +542,8 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da let actionsDisposable = DisposableSet() - let cacheUsagePromise = Promise() - cacheUsagePromise.set(cacheUsageStats(context: context)) + //let cacheUsagePromise = Promise() + //cacheUsagePromise.set(cacheUsageStats(context: context)) let updateSensitiveContentDisposable = MetaDisposable() actionsDisposable.add(updateSensitiveContentDisposable) @@ -594,7 +595,10 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da }) let arguments = DataAndStorageControllerArguments(openStorageUsage: { - pushControllerImpl?(storageUsageController(context: context, cacheUsagePromise: cacheUsagePromise)) + pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) + //pushControllerImpl?(storageUsageController(context: context, cacheUsagePromise: cacheUsagePromise)) }, openNetworkUsage: { pushControllerImpl?(networkUsageStatsController(context: context)) }, openProxy: { diff --git a/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift index cbe34dac81b..236e569a704 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift @@ -18,11 +18,9 @@ private func stringForKeepMediaTimeout(strings: PresentationStrings, timeout: In } } -// MARK: Nicegram CacheSettings, (added 1 hour, 1 day; removed 1 week) private let keepMediaTimeoutValues: [Int32] = [ - 1 * 60 * 60, - 1 * 24 * 60 * 60, 3 * 24 * 60 * 60, + 7 * 24 * 60 * 60, 1 * 31 * 24 * 60 * 60, Int32.max ] @@ -101,8 +99,7 @@ private final class KeepMediaDurationPickerItemNode: ListViewItemNode { self.maskNode = ASImageNode() var textNodes: [TextNode] = [] - // MARK: Nicegram CacheSettings, change constant to actual value - for _ in 0 ..< keepMediaTimeoutValues.count { + for _ in 0 ..< 4 { let textNode = TextNode() textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false @@ -119,10 +116,8 @@ private final class KeepMediaDurationPickerItemNode: ListViewItemNode { func updateSliderView() { if let sliderView = self.sliderView, let item = self.item { - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.maximumValue = CGFloat(keepMediaTimeoutValues.count - 1) - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.positionsCount = keepMediaTimeoutValues.count + sliderView.maximumValue = 3.0 + sliderView.positionsCount = 4 let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0 sliderView.value = CGFloat(value) @@ -138,12 +133,10 @@ private final class KeepMediaDurationPickerItemNode: ListViewItemNode { sliderView.lineSize = 4.0 sliderView.dotSize = 5.0 sliderView.minimumValue = 0.0 - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.maximumValue = CGFloat(keepMediaTimeoutValues.count - 1) + sliderView.maximumValue = 3.0 sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.positionsCount = keepMediaTimeoutValues.count + sliderView.positionsCount = 4 sliderView.useLinesForPositions = true if let item = self.item, let params = self.layoutParams { let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0 @@ -311,4 +304,3 @@ private final class KeepMediaDurationPickerItemNode: ListViewItemNode { self.item?.updated(value) } } - diff --git a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift index 7b01e0fea18..f9df190a5e9 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift @@ -30,17 +30,13 @@ private func stringForCacheSize(strings: PresentationStrings, size: Int32) -> St private let maximumCacheSizeValues: [Int32] = { let diskSpace = totalDiskSpace() if diskSpace > 100 * 1024 * 1024 * 1024 { - // MARK: Nicegram CacheSettings, custom values - return [1, 2, 16, 32, Int32.max] + return [5, 20, 50, Int32.max] } else if diskSpace > 50 * 1024 * 1024 * 1024 { - // MARK: Nicegram CacheSettings, custom values - return [1, 2, 4, 16, Int32.max] + return [5, 16, 32, Int32.max] } else if diskSpace > 24 * 1024 * 1024 * 1024 { - // MARK: Nicegram CacheSettings, custom values - return [1, 2, 4, 16, Int32.max] + return [2, 8, 16, Int32.max] } else { - // MARK: Nicegram CacheSettings, custom values - return [1, 2, 4, 8, Int32.max] + return [1, 4, 8, Int32.max] } }() @@ -118,8 +114,7 @@ private final class MaximumCacheSizePickerItemNode: ListViewItemNode { self.maskNode = ASImageNode() var textNodes: [TextNode] = [] - // MARK: Nicegram CacheSettings, change constant to actual value - for _ in 0 ..< maximumCacheSizeValues.count { + for _ in 0 ..< 4 { let textNode = TextNode() textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false @@ -136,10 +131,8 @@ private final class MaximumCacheSizePickerItemNode: ListViewItemNode { func updateSliderView() { if let sliderView = self.sliderView, let item = self.item { - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.maximumValue = CGFloat(maximumCacheSizeValues.count - 1) - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.positionsCount = maximumCacheSizeValues.count + sliderView.maximumValue = 3.0 + sliderView.positionsCount = 4 let value = maximumCacheSizeValues.firstIndex(where: { $0 == item.value }) ?? 0 sliderView.value = CGFloat(value) @@ -155,12 +148,10 @@ private final class MaximumCacheSizePickerItemNode: ListViewItemNode { sliderView.lineSize = 4.0 sliderView.dotSize = 5.0 sliderView.minimumValue = 0.0 - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.maximumValue = CGFloat(maximumCacheSizeValues.count - 1) + sliderView.maximumValue = 3.0 sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true - // MARK: Nicegram CacheSettings, change constant to actual value - sliderView.positionsCount = maximumCacheSizeValues.count + sliderView.positionsCount = 4 sliderView.useLinesForPositions = true if let item = self.item, let params = self.layoutParams { let value = maximumCacheSizeValues.firstIndex(where: { $0 == item.value }) ?? 0 @@ -328,4 +319,3 @@ private final class MaximumCacheSizePickerItemNode: ListViewItemNode { self.item?.updated(value) } } - diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index d06be8a8ce9..222a3827435 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -17,6 +17,8 @@ import DeleteChatPeerActionSheetItem import UndoUI import AnimatedStickerNode import TelegramAnimatedStickerNode +import ContextUI +import AnimatedAvatarSetNode private func totalDiskSpace() -> Int64 { do { @@ -44,8 +46,9 @@ private final class StorageUsageControllerArguments { let openPeerMedia: (PeerId) -> Void let clearPeerMedia: (PeerId) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let openCategoryMenu: (StorageUsageEntryTag) -> Void - init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, updateMaximumCacheSize: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) { + init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, updateMaximumCacheSize: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, openCategoryMenu: @escaping (StorageUsageEntryTag) -> Void) { self.context = context self.updateKeepMediaTimeout = updateKeepMediaTimeout self.updateMaximumCacheSize = updateMaximumCacheSize @@ -53,6 +56,7 @@ private final class StorageUsageControllerArguments { self.openPeerMedia = openPeerMedia self.clearPeerMedia = clearPeerMedia self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.openCategoryMenu = openCategoryMenu } } @@ -63,8 +67,27 @@ private enum StorageUsageSection: Int32 { case peers } +private enum StorageUsageEntryTag: Hashable, ItemListItemTag { + case privateChats + case groups + case channels + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? StorageUsageEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum StorageUsageEntry: ItemListNodeEntry { case keepMediaHeader(PresentationTheme, String) + + case keepMediaPrivateChats(title: String, text: String?, value: String) + case keepMediaGroups(title: String, text: String?, value: String) + case keepMediaChannels(title: String, text: String?, value: String) + case keepMedia(PresentationTheme, PresentationStrings, Int32) case keepMediaInfo(PresentationTheme, String) @@ -82,43 +105,49 @@ private enum StorageUsageEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .keepMediaHeader, .keepMedia, .keepMediaInfo: - return StorageUsageSection.keepMedia.rawValue - case .maximumSizeHeader, .maximumSize, .maximumSizeInfo: - return StorageUsageSection.maximumSize.rawValue - case .storageHeader, .storageUsage, .collecting, .clearAll: - return StorageUsageSection.storage.rawValue - case .peersHeader, .peer: - return StorageUsageSection.peers.rawValue + case .keepMediaHeader, .keepMedia, .keepMediaInfo, .keepMediaPrivateChats, .keepMediaGroups, .keepMediaChannels: + return StorageUsageSection.keepMedia.rawValue + case .maximumSizeHeader, .maximumSize, .maximumSizeInfo: + return StorageUsageSection.maximumSize.rawValue + case .storageHeader, .storageUsage, .collecting, .clearAll: + return StorageUsageSection.storage.rawValue + case .peersHeader, .peer: + return StorageUsageSection.peers.rawValue } } var stableId: Int32 { switch self { - case .keepMediaHeader: - return 0 - case .keepMedia: - return 1 - case .keepMediaInfo: - return 2 - case .maximumSizeHeader: - return 3 - case .maximumSize: - return 4 - case .maximumSizeInfo: - return 5 - case .storageHeader: - return 6 - case .storageUsage: - return 7 - case .collecting: - return 8 - case .clearAll: - return 9 - case .peersHeader: - return 10 - case let .peer(index, _, _, _, _, _, _, _, _): - return 11 + index + case .keepMediaHeader: + return 0 + case .keepMedia: + return 1 + case .keepMediaPrivateChats: + return 2 + case .keepMediaGroups: + return 3 + case .keepMediaChannels: + return 4 + case .keepMediaInfo: + return 5 + case .maximumSizeHeader: + return 6 + case .maximumSize: + return 7 + case .maximumSizeInfo: + return 8 + case .storageHeader: + return 9 + case .storageUsage: + return 10 + case .collecting: + return 11 + case .clearAll: + return 12 + case .peersHeader: + return 13 + case let .peer(index, _, _, _, _, _, _, _, _): + return 14 + index } } @@ -142,6 +171,24 @@ private enum StorageUsageEntry: ItemListNodeEntry { } else { return false } + case let .keepMediaPrivateChats(title, text, value): + if case .keepMediaPrivateChats(title, text, value) = rhs { + return true + } else { + return false + } + case let .keepMediaGroups(title, text, value): + if case .keepMediaGroups(title, text, value) = rhs { + return true + } else { + return false + } + case let .keepMediaChannels(title, text, value): + if case .keepMediaChannels(title, text, value) = rhs { + return true + } else { + return false + } case let .maximumSizeHeader(lhsTheme, lhsText): if case let .maximumSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -235,6 +282,18 @@ private enum StorageUsageEntry: ItemListNodeEntry { switch self { case let .keepMediaHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .keepMediaPrivateChats(title, text, value): + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/EditProfile")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + arguments.openCategoryMenu(.privateChats) + }, tag: StorageUsageEntryTag.privateChats) + case let .keepMediaGroups(title, text, value): + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/GroupChats")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + arguments.openCategoryMenu(.groups) + }, tag: StorageUsageEntryTag.groups) + case let .keepMediaChannels(title, text, value): + return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + arguments.openCategoryMenu(.channels) + }, tag: StorageUsageEntryTag.channels) case let .keepMedia(theme, strings, value): return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in arguments.updateKeepMediaTimeout(updatedValue) @@ -279,18 +338,46 @@ private enum StorageUsageEntry: ItemListNodeEntry { } private struct StorageUsageState: Equatable { - let peerIdWithRevealedOptions: PeerId? - - func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> StorageUsageState { - return StorageUsageState(peerIdWithRevealedOptions: peerIdWithRevealedOptions) - } + var peerIdWithRevealedOptions: PeerId? } -private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] { +private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, accountSpecificCacheSettings: AccountSpecificCacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] { var entries: [StorageUsageEntry] = [] entries.append(.keepMediaHeader(presentationData.theme, presentationData.strings.Cache_KeepMedia.uppercased())) - entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout)) + + let sections: [StorageUsageEntryTag] = [.privateChats, .groups, .channels] + for section in sections { + let mappedCategory: CacheStorageSettings.PeerStorageCategory + switch section { + case .privateChats: + mappedCategory = .privateChats + case .groups: + mappedCategory = .groups + case .channels: + mappedCategory = .channels + } + let value = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max + + let optionText: String + if value == Int32.max { + optionText = presentationData.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: presentationData.strings, value: value) + } + + switch section { + case .privateChats: + entries.append(.keepMediaPrivateChats(title: presentationData.strings.Notifications_PrivateChats, text: nil, value: optionText)) + case .groups: + entries.append(.keepMediaGroups(title: presentationData.strings.Notifications_GroupChats, text: nil, value: optionText)) + case .channels: + entries.append(.keepMediaChannels(title: presentationData.strings.Notifications_Channels, text: nil, value: optionText)) + } + } + + //entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout)) + entries.append(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_KeepMediaHelp)) entries.append(.maximumSizeHeader(presentationData.theme, presentationData.strings.Cache_MaximumCacheSize.uppercased())) @@ -420,7 +507,24 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P return cacheSettings }) + let accountSpecificCacheSettingsPromise = Promise() + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + accountSpecificCacheSettingsPromise.set(context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + }) + var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + var findAutoremoveReferenceNode: ((StorageUsageEntryTag) -> ItemListDisclosureItemNode?)? + var presentInGlobalOverlay: ((ViewController) -> Void)? var statsPromise: Promise if let cacheUsagePromise = cacheUsagePromise { @@ -441,11 +545,15 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return current.withUpdatedDefaultCacheStorageTimeout(value) + var current = current + current.defaultCacheStorageTimeout = value + return current }).start() }, updateMaximumCacheSize: { value in let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in - return current.withUpdatedDefaultCacheStorageLimitGigabytes(value) + var current = current + current.defaultCacheStorageLimitGigabytes = value + return current }).start() }, openClearAll: { let _ = (statsPromise.get() @@ -957,28 +1065,200 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P }) updateState { state in - return state.withUpdatedPeerIdWithRevealedOptions(nil) + var state = state + state.peerIdWithRevealedOptions = nil + return state } }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { - return state.withUpdatedPeerIdWithRevealedOptions(peerId) + var state = state + state.peerIdWithRevealedOptions = peerId + return state } else { return state } } + }, openCategoryMenu: { category in + let mappedCategory: CacheStorageSettings.PeerStorageCategory + switch category { + case .privateChats: + mappedCategory = .privateChats + case .groups: + mappedCategory = .groups + case .channels: + mappedCategory = .channels + } + + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + let accountSpecificSettings: Signal = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + } + |> distinctUntilChanged + + let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings + |> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in + return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in + var result: [(peer: FoundPeer, value: Int32)] = [] + + for item in accountSpecificSettings.peerStorageTimeoutExceptions { + let peerId = item.key + let value = item.value + + guard let peer = transaction.getPeer(peerId) else { + continue + } + let peerCategory: CacheStorageSettings.PeerStorageCategory + var subscriberCount: Int32? + if peer is TelegramUser { + peerCategory = .privateChats + } else if peer is TelegramGroup { + peerCategory = .groups + + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init) + } + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + peerCategory = .groups + } else { + peerCategory = .channels + } + if peerCategory == mappedCategory { + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + subscriberCount = cachedData.participantsSummary.memberCount + } + } + } else { + continue + } + + if peerCategory != mappedCategory { + continue + } + + result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) + } + + return result.sorted(by: { lhs, rhs in + if lhs.value != rhs.value { + return lhs.value < rhs.value + } + return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle + }) + } + } + + let _ = (combineLatest( + cacheSettingsPromise.get() |> take(1), + peerExceptions |> take(1) + ) + |> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in + let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max + + let applyValue: (Int32) -> Void = { value in + let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in + var cacheSettings = cacheSettings + cacheSettings.categoryStorageTimeout[mappedCategory] = value + return cacheSettings + }).start() + } + + var subItems: [ContextMenuItem] = [] + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var presetValues: [Int32] = [ + Int32.max, + 31 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + 1 * 24 * 60 * 60 + ] + if currentValue != 0 && !presetValues.contains(currentValue) { + presetValues.append(currentValue) + presetValues.sort(by: >) + } + + for value in presetValues { + let optionText: String + if value == Int32.max { + optionText = presentationData.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: presentationData.strings, value: value) + } + subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in + if currentValue == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, f in + applyValue(value) + f(.default) + }))) + } + + subItems.append(.separator) + + if peerExceptions.isEmpty { + let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException + subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in + if case .privateChats = category { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor) + } + }, action: { _, f in + f(.default) + + pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory)) + }))) + } else { + subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, action: { c, _ in + c.dismiss(completion: { + + }) + pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory)) + }), false)) + } + + if let sourceNode = findAutoremoveReferenceNode?(category) { + let items: Signal = .single(ContextController.Items(content: .list(subItems))) + let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceNode.labelNode.view)) + + let contextController = ContextController( + account: context.account, + presentationData: presentationData, + source: source, + items: items, + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlay?(contextController) + } + }) }) var dismissImpl: (() -> Void)? - let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue - |> map { presentationData, cacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), accountSpecificCacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue + |> map { presentationData, cacheSettings, accountSpecificCacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) : nil let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, accountSpecificCacheSettings: accountSpecificCacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -993,6 +1273,34 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P presentControllerImpl = { [weak controller] c, contextType, a in controller?.present(c, in: contextType, with: a) } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + presentInGlobalOverlay = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } + findAutoremoveReferenceNode = { [weak controller] category in + guard let controller else { + return nil + } + + let targetTag: StorageUsageEntryTag = category + var resultItemNode: ItemListItemNode? + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode { + if let tag = itemNode.tag, tag.isEqual(to: targetTag) { + resultItemNode = itemNode + return + } + } + } + + if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode { + return resultItemNode + } else { + return nil + } + } dismissImpl = { [weak controller] in controller?.dismiss() } @@ -1110,3 +1418,215 @@ private class StorageUsageClearProgressOverlayNode: ASDisplayNode, ActionSheetGr self.animationNode.updateLayout(size: imageSize) } } + +private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + +final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem { + fileprivate let context: AccountContext + fileprivate let peers: [EnginePeer] + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void + + init(context: AccountContext, peers: [EnginePeer], action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { + self.context = context + self.peers = peers + self.action = action + } + + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol { + private let item: MultiplePeerAvatarsContextItem + private var presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + private let avatarsNode: AnimatedAvatarSetNode + private let avatarsContext: AnimatedAvatarSetContext + + private let buttonNode: HighlightTrackingButtonNode + + private var pointerInteraction: PointerInteraction? + + init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isAccessibilityElement = false + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.textNode.maximumNumberOfLines = 1 + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isAccessibilityElement = true + self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording + + self.avatarsNode = AnimatedAvatarSetNode() + self.avatarsContext = AnimatedAvatarSetContext() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.textNode) + self.addSubnode(self.avatarsNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.isUserInteractionEnabled = true + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.75 + } + }, willExit: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + } + }) + } + + private var validLayout: (calculatedWidth: CGFloat, size: CGSize)? + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 14.0 + let verticalInset: CGFloat = 12.0 + + let rightTextInset: CGFloat = sideInset + 36.0 + + let calculatedWidth = min(constrainedWidth, 250.0) + + let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize) + let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.peers.count)) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + + let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) + + let combinedTextHeight = textSize.height + return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in + self.validLayout = (calculatedWidth: calculatedWidth, size: size) + let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + let avatarsContent: AnimatedAvatarSetContext.Content + + let avatarsPeers: [EnginePeer] = self.item.peers + + avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false) + + let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) + self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + } + + @objc private func buttonPressed() { + self.performAction() + } + + private var actionTemporarilyDisabled: Bool = false + + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + + func performAction() { + if self.actionTemporarilyDisabled { + return + } + self.actionTemporarilyDisabled = true + Queue.mainQueue().async { [weak self] in + self?.actionTemporarilyDisabled = false + } + + guard let controller = self.getController() else { + return + } + self.item.action(controller, { [weak self] result in + self?.actionSelected(result) + }) + } + + var isActionEnabled: Bool { + return true + } + + func setIsHighlighted(_ value: Bool) { + if value { + self.highlightedBackgroundNode.alpha = 1.0 + } else { + self.highlightedBackgroundNode.alpha = 0.0 + } + } + + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { + return self + } +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift new file mode 100644 index 00000000000..6cf80c25109 --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -0,0 +1,508 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TelegramStringFormatting +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import ItemListPeerItem +import UndoUI +import ContextUI +import ItemListPeerActionItem + +private enum StorageUsageExceptionsEntryTag: Hashable, ItemListItemTag { + case peer(EnginePeer.Id) + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? StorageUsageExceptionsEntryTag, self == other { + return true + } else { + return false + } + } +} + +private final class StorageUsageExceptionsScreenArguments { + let context: AccountContext + let openAddException: () -> Void + let openPeerMenu: (EnginePeer.Id, Int32) -> Void + + init( + context: AccountContext, + openAddException: @escaping () -> Void, + openPeerMenu: @escaping (EnginePeer.Id, Int32) -> Void + ) { + self.context = context + self.openAddException = openAddException + self.openPeerMenu = openPeerMenu + } +} + +private enum StorageUsageExceptionsSection: Int32 { + case add + case items +} + +private enum StorageUsageExceptionsEntry: ItemListNodeEntry { + enum SortIndex: Equatable, Comparable { + case index(Int) + case peer(index: Int, peerId: EnginePeer.Id) + + static func <(lhs: SortIndex, rhs: SortIndex) -> Bool { + switch lhs { + case let .index(index): + if case let .index(rhsIndex) = rhs { + return index < rhsIndex + } else { + return true + } + case let .peer(index, peerId): + if case let .peer(rhsIndex, rhsPeerId) = rhs { + if index != rhsIndex { + return index < rhsIndex + } else { + return peerId < rhsPeerId + } + } else { + return false + } + } + } + } + + enum StableId: Hashable { + case index(Int) + case peer(EnginePeer.Id) + } + + case addException(String) + case exceptionsHeader(String) + case peer(index: Int, peer: FoundPeer, value: Int32) + + var section: ItemListSectionId { + switch self { + case .addException: + return StorageUsageExceptionsSection.add.rawValue + case .exceptionsHeader, .peer: + return StorageUsageExceptionsSection.items.rawValue + } + } + + var stableId: StableId { + switch self { + case .addException: + return .index(0) + case .exceptionsHeader: + return .index(1) + case let .peer(_, peer, _): + return .peer(peer.peer.id) + } + } + + var sortIndex: SortIndex { + switch self { + case .addException: + return .index(0) + case .exceptionsHeader: + return .index(1) + case let .peer(index, peer, _): + return .peer(index: index, peerId: peer.peer.id) + } + } + + static func ==(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool { + switch lhs { + case let .addException(text): + if case .addException(text) = rhs { + return true + } else { + return false + } + case let .exceptionsHeader(text): + if case .exceptionsHeader(text) = rhs { + return true + } else { + return false + } + case let .peer(index, peer, value): + if case .peer(index, peer, value) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool { + return lhs.sortIndex < rhs.sortIndex + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! StorageUsageExceptionsScreenArguments + switch self { + case let .addException(text): + let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme) + return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: { + arguments.openAddException() + }) + case let .exceptionsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .peer(_, peer, value): + var additionalDetailLabel: String? + if let subscribers = peer.subscribers { + additionalDetailLabel = presentationData.strings.VoiceChat_Panel_Members(subscribers) + } + let optionText: String + if value == Int32.max { + optionText = presentationData.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: presentationData.strings, value: value) + } + + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + arguments.openPeerMenu(peer.peer.id, value) + }, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id)) + } + } +} + +private struct StorageUsageExceptionsState: Equatable { +} + +private func storageUsageExceptionsScreenEntries( + presentationData: PresentationData, + peerExceptions: [(peer: FoundPeer, value: Int32)], + state: StorageUsageExceptionsState +) -> [StorageUsageExceptionsEntry] { + var entries: [StorageUsageExceptionsEntry] = [] + + entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException)) + + if !peerExceptions.isEmpty { + entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(peerExceptions.count)).uppercased())) + + var index = 100 + for item in peerExceptions { + entries.append(.peer(index: index, peer: item.peer, value: item.value)) + index += 1 + } + } + + return entries +} + +public func storageUsageExceptionsScreen( + context: AccountContext, + category: CacheStorageSettings.PeerStorageCategory, + isModal: Bool = false +) -> ViewController { + let statePromise = ValuePromise(StorageUsageExceptionsState()) + let stateValue = Atomic(value: StorageUsageExceptionsState()) + let updateState: ((StorageUsageExceptionsState) -> StorageUsageExceptionsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + let _ = updateState + + let cacheSettingsPromise = Promise() + cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> map { sharedData -> CacheStorageSettings in + let cacheSettings: CacheStorageSettings + if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = CacheStorageSettings.defaultSettings + } + + return cacheSettings + }) + + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + let accountSpecificSettings: Signal = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + } + |> distinctUntilChanged + + let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings + |> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in + return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in + var result: [(peer: FoundPeer, value: Int32)] = [] + + for item in accountSpecificSettings.peerStorageTimeoutExceptions { + let peerId = item.key + let value = item.value + + guard let peer = transaction.getPeer(peerId) else { + continue + } + let peerCategory: CacheStorageSettings.PeerStorageCategory + var subscriberCount: Int32? + if peer is TelegramUser { + peerCategory = .privateChats + } else if peer is TelegramGroup { + peerCategory = .groups + + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init) + } + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + peerCategory = .groups + } else { + peerCategory = .channels + } + if peerCategory == category { + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + subscriberCount = cachedData.participantsSummary.memberCount + } + } + } else { + continue + } + + if peerCategory != category { + continue + } + + result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) + } + + return result.sorted(by: { lhs, rhs in + if lhs.value != rhs.value { + return lhs.value < rhs.value + } + return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle + }) + } + } + + var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)? + let _ = presentControllerImpl + var pushControllerImpl: ((ViewController) -> Void)? + + var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)? + let _ = findPeerReferenceNode + + var presentInGlobalOverlay: ((ViewController) -> Void)? + let _ = presentInGlobalOverlay + + let actionDisposables = DisposableSet() + + let clearDisposable = MetaDisposable() + actionDisposables.add(clearDisposable) + + let arguments = StorageUsageExceptionsScreenArguments( + context: context, + openAddException: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] + switch category { + case .groups: + filter.insert(.onlyGroups) + case .privateChats: + filter.insert(.onlyPrivateChats) + filter.insert(.excludeSecretChats) + case .channels: + filter.insert(.onlyChannels) + } + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle)) + controller.peerSelected = { [weak controller] peer, _ in + let peerId = peer.id + + let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + + for i in 0 ..< settings.peerStorageTimeoutExceptions.count { + if settings.peerStorageTimeoutExceptions[i].key == peerId { + settings.peerStorageTimeoutExceptions.remove(at: i) + break + } + } + settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: Int32.max)) + + return settings + }).start() + + controller?.dismiss() + } + pushControllerImpl?(controller) + }, + openPeerMenu: { peerId, currentValue in + let applyValue: (Int32?) -> Void = { value in + let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + + if let value = value { + var found = false + for i in 0 ..< settings.peerStorageTimeoutExceptions.count { + if settings.peerStorageTimeoutExceptions[i].key == peerId { + found = true + settings.peerStorageTimeoutExceptions[i] = AccountSpecificCacheStorageSettings.Value(key: peerId, value: value) + break + } + } + if !found { + settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: value)) + } + } else { + for i in 0 ..< settings.peerStorageTimeoutExceptions.count { + if settings.peerStorageTimeoutExceptions[i].key == peerId { + settings.peerStorageTimeoutExceptions.remove(at: i) + break + } + } + } + + return settings + }).start() + } + + var subItems: [ContextMenuItem] = [] + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let presetValues: [Int32] = [ + Int32.max, + 31 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + 1 * 24 * 60 * 60 + ] + + for value in presetValues { + let optionText: String + if value == Int32.max { + optionText = presentationData.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: presentationData.strings, value: value) + } + subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in + if currentValue == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, f in + applyValue(value) + f(.default) + }))) + } + + subItems.append(.separator) + subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { _, f in + f(.default) + + applyValue(nil) + }))) + + if let sourceNode = findPeerReferenceNode?(peerId) { + let items: Signal = .single(ContextController.Items(content: .list(subItems))) + let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)) + + let contextController = ContextController( + account: context.account, + presentationData: presentationData, + source: source, + items: items, + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlay?(contextController) + } + } + ) + + let _ = cacheSettingsPromise + + var dismissImpl: (() -> Void)? + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + peerExceptions, + statePromise.get() + ) + |> deliverOnMainQueue + |> map { presentationData, peerExceptions, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) : nil + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageExceptionsScreenEntries(presentationData: presentationData, peerExceptions: peerExceptions, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionDisposables.dispose() + } + + let controller = ItemListController(context: context, state: signal) + if isModal { + controller.navigationPresentation = .modal + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + presentControllerImpl = { [weak controller] c, contextType, a in + controller?.present(c, in: contextType, with: a) + } + pushControllerImpl = { [weak controller] c in + controller?.push(c) + } + presentInGlobalOverlay = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } + findPeerReferenceNode = { [weak controller] peerId in + guard let controller else { + return nil + } + + let targetTag: StorageUsageExceptionsEntryTag = .peer(peerId) + var resultItemNode: ItemListItemNode? + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode { + if let tag = itemNode.tag, tag.isEqual(to: targetTag) { + resultItemNode = itemNode + return + } + } + } + + if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode { + return resultItemNode + } else { + return nil + } + } + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return controller +} + +private final class StorageUsageExceptionsContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index e1781b75e2f..cb687a2578f 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -200,8 +200,8 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo |> take(1) |> deliverOnMainQueue ).start(next: { accountAndPeer, accountsAndPeers in - // MARK: Nicegram max accounts - let maximumAvailableAccounts: Int = 100 + // MARK: Nicegram MaxAccounts + let maximumAvailableAccounts: Int = nicegramMaximumNumberOfAccounts var count: Int = 1 for (accountContext, _, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 7e04017a087..ccda65743c0 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -139,8 +139,8 @@ public func logoutOptionsController(context: AccountContext, navigationControlle |> take(1) |> deliverOnMainQueue ).start(next: { accountAndPeer, accountsAndPeers in - // MARK: Nicegram max accounts - let maximumAvailableAccounts: Int = 100 + // MARK: Nicegram MaxAccounts + let maximumAvailableAccounts: Int = nicegramMaximumNumberOfAccounts var count: Int = 1 for (accountContext, _, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 6c7175ce580..d204760334a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -614,7 +614,22 @@ class PrivacyAndSecurityControllerImpl: ItemListController, ASAuthorizationContr } } -public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil, loginEmailPattern: Signal? = nil, updatedTwoStepAuthData: (() -> Void)? = nil) -> ViewController { +public func privacyAndSecurityController( + context: AccountContext, + initialSettings: AccountPrivacySettings? = nil, + updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, + updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, + updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, + focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, + activeSessionsContext: ActiveSessionsContext? = nil, + webSessionsContext: WebSessionsContext? = nil, + blockedPeersContext: BlockedPeersContext? = nil, + hasTwoStepAuth: Bool? = nil, + loginEmailPattern: Signal? = nil, + updatedTwoStepAuthData: (() -> Void)? = nil, + requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil, + requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil +) -> ViewController { let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in @@ -804,7 +819,11 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl?(selectivePrivacySettingsController(context: context, kind: .profilePhoto, current: info.profilePhoto, updated: { updated, _, _ in + pushControllerImpl?(selectivePrivacySettingsController(context: context, kind: .profilePhoto, current: info.profilePhoto, requestPublicPhotoSetup: { completion in + requestPublicPhotoSetup?(completion) + }, requestPublicPhotoRemove: { completion in + requestPublicPhotoRemove?(completion) + }, updated: { updated, _, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift index 4fafef3f8eb..73a50a6592a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift @@ -44,14 +44,14 @@ public enum PrivacyIntroControllerMode { } } - func title(strings: PresentationStrings) -> String { + func title(context: AccountContext, strings: PresentationStrings) -> String { switch self { case .passcode: return strings.PasscodeSettings_Title case .twoStepVerification: return strings.TwoStepAuth_AdditionalPassword case let .changePhoneNumber(phoneNumber): - return formatPhoneNumber(phoneNumber) + return formatPhoneNumber(context: context, number: phoneNumber) } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift index 327ee55a34a..66a1e65b1aa 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift @@ -118,7 +118,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode { if self.animationNode.isHidden { self.iconNode.image = self.mode.icon(theme: presentationData.theme) } - self.titleNode.attributedText = NSAttributedString(string: self.mode.title(strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center) + self.titleNode.attributedText = NSAttributedString(string: self.mode.title(context: self.context, strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: self.mode.text(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center) self.noticeNode.attributedText = NSAttributedString(string: self.mode.notice(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center) self.buttonTextNode.attributedText = NSAttributedString(string: self.mode.buttonTitle(strings: presentationData.strings), font: buttonFont, textColor: presentationData.theme.list.itemAccentColor, paragraphAlignment: .center) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 96780ac3f8a..734fc07394f 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -10,6 +10,8 @@ import ItemListUI import PresentationDataUtils import AccountContext import UndoUI +import ItemListPeerActionItem +import AvatarNode enum SelectivePrivacySettingsKind { case presence @@ -54,7 +56,10 @@ private final class SelectivePrivacySettingsControllerArguments { let updatePhoneDiscovery: ((Bool) -> Void)? let copyPhoneLink: ((String) -> Void)? - init(context: AccountContext, updateType: @escaping (SelectivePrivacySettingType) -> Void, openSelective: @escaping (SelectivePrivacySettingsPeerTarget, Bool) -> Void, updateCallP2PMode: ((SelectivePrivacySettingType) -> Void)?, updateCallIntegrationEnabled: ((Bool) -> Void)?, updatePhoneDiscovery: ((Bool) -> Void)?, copyPhoneLink: ((String) -> Void)?) { + let setPublicPhoto: (() -> Void)? + let removePublicPhoto: (() -> Void)? + + init(context: AccountContext, updateType: @escaping (SelectivePrivacySettingType) -> Void, openSelective: @escaping (SelectivePrivacySettingsPeerTarget, Bool) -> Void, updateCallP2PMode: ((SelectivePrivacySettingType) -> Void)?, updateCallIntegrationEnabled: ((Bool) -> Void)?, updatePhoneDiscovery: ((Bool) -> Void)?, copyPhoneLink: ((String) -> Void)?, setPublicPhoto: (() -> Void)?, removePublicPhoto: (() -> Void)?) { self.context = context self.updateType = updateType self.openSelective = openSelective @@ -63,6 +68,9 @@ private final class SelectivePrivacySettingsControllerArguments { self.updateCallIntegrationEnabled = updateCallIntegrationEnabled self.updatePhoneDiscovery = updatePhoneDiscovery self.copyPhoneLink = copyPhoneLink + + self.setPublicPhoto = setPublicPhoto + self.removePublicPhoto = removePublicPhoto } } @@ -74,6 +82,7 @@ private enum SelectivePrivacySettingsSection: Int32 { case callsP2PPeers case callsIntegrationEnabled case phoneDiscovery + case photo } private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String { @@ -114,6 +123,9 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case phoneDiscoveryEverybody(PresentationTheme, String, Bool) case phoneDiscoveryMyContacts(PresentationTheme, String, Bool) case phoneDiscoveryInfo(PresentationTheme, String, String) + case setPublicPhoto(PresentationTheme, String) + case removePublicPhoto(PresentationTheme, String, EnginePeer, TelegramMediaImage?, UIImage?) + case publicPhotoInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -131,6 +143,8 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return SelectivePrivacySettingsSection.callsIntegrationEnabled.rawValue case .phoneDiscoveryHeader, .phoneDiscoveryEverybody, .phoneDiscoveryMyContacts, .phoneDiscoveryInfo: return SelectivePrivacySettingsSection.phoneDiscovery.rawValue + case .setPublicPhoto, .removePublicPhoto, .publicPhotoInfo: + return SelectivePrivacySettingsSection.photo.rawValue } } @@ -186,6 +200,12 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return 23 case .callsIntegrationInfo: return 24 + case .setPublicPhoto: + return 24 + case .removePublicPhoto: + return 25 + case .publicPhotoInfo: + return 26 } } @@ -222,7 +242,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return false } case let .nobody(lhsTheme, lhsText, lhsValue): - if case let nobody(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + if case let .nobody(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -341,6 +361,24 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { } else { return false } + case let .setPublicPhoto(lhsTheme, lhsText): + if case let .setPublicPhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .removePublicPhoto(lhsTheme, lhsText, lhsPeer, lhsRep, lhsImage): + if case let .removePublicPhoto(rhsTheme, rhsText, rhsPeer, rhsRep, rhsImage) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsPeer == rhsPeer, lhsRep == rhsRep, lhsImage === rhsImage { + return true + } else { + return false + } + case let .publicPhotoInfo(lhsTheme, lhsText): + if case let .publicPhotoInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -431,6 +469,17 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.copyPhoneLink?(link) }) + case let .setPublicPhoto(theme, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.setPublicPhoto?() + }) + case let .removePublicPhoto(_, text, peer, image, completeImage): + return ItemListPeerActionItem(presentationData: presentationData, icon: completeImage, iconSignal: completeImage == nil ? peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)) : nil, title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + arguments.removePublicPhoto?() + }) + case let .publicPhotoInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in + }) } } } @@ -449,8 +498,10 @@ private struct SelectivePrivacySettingsControllerState: Equatable { let callIntegrationAvailable: Bool? let callIntegrationEnabled: Bool? let phoneDiscoveryEnabled: Bool? + + let uploadedPhoto: UIImage? - init(setting: SelectivePrivacySettingType, enableFor: [PeerId: SelectivePrivacyPeer], disableFor: [PeerId: SelectivePrivacyPeer], saving: Bool, callDataSaving: VoiceCallDataSaving?, callP2PMode: SelectivePrivacySettingType?, callP2PEnableFor: [PeerId: SelectivePrivacyPeer]?, callP2PDisableFor: [PeerId: SelectivePrivacyPeer]?, callIntegrationAvailable: Bool?, callIntegrationEnabled: Bool?, phoneDiscoveryEnabled: Bool?) { + init(setting: SelectivePrivacySettingType, enableFor: [PeerId: SelectivePrivacyPeer], disableFor: [PeerId: SelectivePrivacyPeer], saving: Bool, callDataSaving: VoiceCallDataSaving?, callP2PMode: SelectivePrivacySettingType?, callP2PEnableFor: [PeerId: SelectivePrivacyPeer]?, callP2PDisableFor: [PeerId: SelectivePrivacyPeer]?, callIntegrationAvailable: Bool?, callIntegrationEnabled: Bool?, phoneDiscoveryEnabled: Bool?, uploadedPhoto: UIImage?) { self.setting = setting self.enableFor = enableFor self.disableFor = disableFor @@ -462,6 +513,7 @@ private struct SelectivePrivacySettingsControllerState: Equatable { self.callIntegrationAvailable = callIntegrationAvailable self.callIntegrationEnabled = callIntegrationEnabled self.phoneDiscoveryEnabled = phoneDiscoveryEnabled + self.uploadedPhoto = uploadedPhoto } static func ==(lhs: SelectivePrivacySettingsControllerState, rhs: SelectivePrivacySettingsControllerState) -> Bool { @@ -498,48 +550,55 @@ private struct SelectivePrivacySettingsControllerState: Equatable { if lhs.phoneDiscoveryEnabled != rhs.phoneDiscoveryEnabled { return false } + if lhs.uploadedPhoto !== rhs.uploadedPhoto { + return false + } return true } func withUpdatedSetting(_ setting: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedEnableFor(_ enableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedDisableFor(_ disableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedSaving(_ saving: Bool) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedCallP2PMode(_ mode: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: mode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: mode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedCallP2PEnableFor(_ enableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: enableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: enableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedCallP2PDisableFor(_ disableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: disableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: disableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedCallsIntegrationEnabled(_ enabled: Bool) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: enabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: enabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) } func withUpdatedPhoneDiscoveryEnabled(_ phoneDiscoveryEnabled: Bool) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: phoneDiscoveryEnabled) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: self.uploadedPhoto) + } + + func withUpdatedUploadedPhoto(_ uploadedPhoto: UIImage?) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled, uploadedPhoto: uploadedPhoto) } } -private func selectivePrivacySettingsControllerEntries(presentationData: PresentationData, kind: SelectivePrivacySettingsKind, state: SelectivePrivacySettingsControllerState, peerName: String, phoneNumber: String) -> [SelectivePrivacySettingsEntry] { +private func selectivePrivacySettingsControllerEntries(presentationData: PresentationData, kind: SelectivePrivacySettingsKind, state: SelectivePrivacySettingsControllerState, peerName: String, phoneNumber: String, peer: EnginePeer?, publicPhoto: TelegramMediaImage?) -> [SelectivePrivacySettingsEntry] { var entries: [SelectivePrivacySettingsEntry] = [] let settingTitle: String @@ -612,11 +671,9 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present entries.append(.contacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.setting == .contacts)) switch kind { // MARK: Nicegram NobodyGroupInvitation, add .groupInvitations - case .presence, .voiceCalls, .forwards, .phoneNumber, .voiceMessages, .groupInvitations: + case .presence, .voiceCalls, .forwards, .phoneNumber, .voiceMessages, .profilePhoto, .groupInvitations: entries.append(.nobody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenNobody, state.setting == .nobody)) // MARK: Nicegram NobodyGroupInvitation, remove .groupInvitations - case .profilePhoto: - break } let phoneLink = "https://t.me/+\(phoneNumber)" if let settingInfoText = settingInfoText { @@ -629,7 +686,7 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false)) entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink)) } - + entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_Exceptions)) switch state.setting { @@ -641,7 +698,20 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present case .nobody: entries.append(.enableFor(presentationData.theme, enableForText, stringForUserCount(state.enableFor, strings: presentationData.strings))) } - entries.append(.peersInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_CustomShareSettingsHelp)) + let exceptionsInfo: String + if case .profilePhoto = kind { + switch state.setting { + case .nobody: + exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideAddInfo + case .contacts: + exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideBothInfo + case .everybody: + exceptionsInfo = presentationData.strings.Privacy_ProfilePhoto_CustomOverrideInfo + } + } else { + exceptionsInfo = presentationData.strings.PrivacyLastSeenSettings_CustomShareSettingsHelp + } + entries.append(.peersInfo(presentationData.theme, exceptionsInfo)) if case .voiceCalls = kind, let p2pMode = state.callP2PMode, let integrationAvailable = state.callIntegrationAvailable, let integrationEnabled = state.callIntegrationEnabled { entries.append(.callsP2PHeader(presentationData.theme, presentationData.strings.Privacy_Calls_P2P.uppercased())) @@ -670,10 +740,31 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present } } + if case .profilePhoto = kind, let peer = peer, state.setting != .everybody || !state.disableFor.isEmpty { + if let publicPhoto = publicPhoto { + entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_UpdatePublicPhoto)) + entries.append(.removePublicPhoto(presentationData.theme, !publicPhoto.videoRepresentations.isEmpty ? presentationData.strings.Privacy_ProfilePhoto_RemovePublicVideo : presentationData.strings.Privacy_ProfilePhoto_RemovePublicPhoto, peer, publicPhoto, state.uploadedPhoto)) + } else { + entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_SetPublicPhoto)) + } + entries.append(.publicPhotoInfo(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_PublicPhotoInfo)) + } + return entries } -func selectivePrivacySettingsController(context: AccountContext, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, callSettings: (SelectivePrivacySettings, VoiceCallSettings)? = nil, phoneDiscoveryEnabled: Bool? = nil, voipConfiguration: VoipConfiguration? = nil, callIntegrationAvailable: Bool? = nil, updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?) -> Void) -> ViewController { +func selectivePrivacySettingsController( + context: AccountContext, + kind: SelectivePrivacySettingsKind, + current: SelectivePrivacySettings, + callSettings: (SelectivePrivacySettings, VoiceCallSettings)? = nil, + phoneDiscoveryEnabled: Bool? = nil, + voipConfiguration: VoipConfiguration? = nil, + callIntegrationAvailable: Bool? = nil, + requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil, + requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil, + updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?) -> Void +) -> ViewController { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings var initialEnableFor: [PeerId: SelectivePrivacyPeer] = [:] @@ -703,7 +794,7 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective } } - let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.1.dataSaving, callP2PMode: callSettings != nil ? SelectivePrivacySettingType(callSettings!.0) : nil, callP2PEnableFor: initialCallP2PEnableFor, callP2PDisableFor: initialCallP2PDisableFor, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.1.enableSystemIntegration, phoneDiscoveryEnabled: phoneDiscoveryEnabled) + let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.1.dataSaving, callP2PMode: callSettings != nil ? SelectivePrivacySettingType(callSettings!.0) : nil, callP2PEnableFor: initialCallP2PEnableFor, callP2PDisableFor: initialCallP2PDisableFor, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.1.enableSystemIntegration, phoneDiscoveryEnabled: phoneDiscoveryEnabled, uploadedPhoto: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -867,43 +958,6 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective } } } - - let controller = selectivePrivacyPeersController(context: context, title: title, initialPeers: updatedPeerIds, updated: { updatedPeerIds in - updateState { state in - if enable { - switch target { - case .main: - var disableFor = state.disableFor - for (key, _) in updatedPeerIds { - disableFor.removeValue(forKey: key) - } - return state.withUpdatedEnableFor(updatedPeerIds).withUpdatedDisableFor(disableFor) - case .callP2P: - var callP2PDisableFor = state.callP2PDisableFor ?? [:] - for (key, _) in updatedPeerIds { - callP2PDisableFor.removeValue(forKey: key) - } - return state.withUpdatedCallP2PEnableFor(updatedPeerIds).withUpdatedCallP2PDisableFor(callP2PDisableFor) - } - } else { - switch target { - case .main: - var enableFor = state.enableFor - for (key, _) in updatedPeerIds { - enableFor.removeValue(forKey: key) - } - return state.withUpdatedDisableFor(updatedPeerIds).withUpdatedEnableFor(enableFor) - case .callP2P: - var callP2PEnableFor = state.callP2PEnableFor ?? [:] - for (key, _) in updatedPeerIds { - callP2PEnableFor.removeValue(forKey: key) - } - return state.withUpdatedCallP2PDisableFor(updatedPeerIds).withUpdatedCallP2PEnableFor(callP2PEnableFor) - } - } - } - }) - pushControllerImpl?(controller, false) }) })) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -967,12 +1021,48 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }, setPublicPhoto: { + requestPublicPhotoSetup?({ result in + var result = result + if let image = result { + result = generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.addPath(CGPath(ellipseIn: CGRect(origin: .zero, size: size), transform: nil)) + context.clip() + if let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + }, opaque: false) + } + updateState { state in + return state.withUpdatedUploadedPhoto(result) + } + }) + }, removePublicPhoto: { + requestPublicPhotoRemove?({ + updateState { state in + return state.withUpdatedUploadedPhoto(nil) + } + }) }) - let peer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let peer = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let publicPhoto = context.account.postbox.peerView(id: context.account.peerId) + |> map { view -> TelegramMediaImage? in + if let cachedUserData = view.cachedData as? CachedUserData, case let .known(photo) = cachedUserData.fallbackPhoto { + return photo + } else { + return nil + } + } - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), peer) |> deliverOnMainQueue - |> map { presentationData, state, peer -> (ItemListControllerState, (ItemListNodeState, Any)) in + let signal = combineLatest( + context.sharedContext.presentationData, + statePromise.get(), + peer, + publicPhoto + ) |> deliverOnMainQueue + |> map { presentationData, state, peer, publicPhoto -> (ItemListControllerState, (ItemListNodeState, Any)) in let peerName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) var phoneNumber = "" if case let .user(user) = peer { @@ -997,7 +1087,7 @@ func selectivePrivacySettingsController(context: AccountContext, kind: Selective title = presentationData.strings.Privacy_VoiceMessages } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state, peerName: peerName ?? "", phoneNumber: phoneNumber), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state, peerName: peerName ?? "", phoneNumber: phoneNumber, peer: peer, publicPhoto: publicPhoto), style: .blocks, animateChanges: true) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 74254c92915..7a816370ccc 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -19,138 +19,174 @@ private final class SelectivePrivacyPeersControllerArguments { let removePeer: (PeerId) -> Void let addPeer: () -> Void let openPeer: (EnginePeer) -> Void + let deleteAll: () -> Void - init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void) { + init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, deleteAll: @escaping () -> Void) { self.context = context self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.addPeer = addPeer self.openPeer = openPeer + self.deleteAll = deleteAll } } private enum SelectivePrivacyPeersSection: Int32 { - case add case peers + case delete } private enum SelectivePrivacyPeersEntryStableId: Hashable { + case header case add case peer(PeerId) + case delete } private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, SelectivePrivacyPeer, ItemListPeerItemEditing, Bool) case addItem(PresentationTheme, String, Bool) + case headerItem(PresentationTheme, String) + case deleteItem(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .peerItem: - return SelectivePrivacyPeersSection.peers.rawValue - case .addItem: - return SelectivePrivacyPeersSection.add.rawValue + case .addItem, .peerItem, .headerItem: + return SelectivePrivacyPeersSection.peers.rawValue + case .deleteItem: + return SelectivePrivacyPeersSection.delete.rawValue } } var stableId: SelectivePrivacyPeersEntryStableId { switch self { - case let .peerItem(_, _, _, _, _, peer, _, _): - return .peer(peer.peer.id) - case .addItem: - return .add + case let .peerItem(_, _, _, _, _, peer, _, _): + return .peer(peer.peer.id) + case .addItem: + return .add + case .headerItem: + return .header + case .deleteItem: + return .delete } } static func ==(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { switch lhs { case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsPeer != rhsPeer { - return false - } - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - if lhsDateTimeFormat != rhsDateTimeFormat { - return false - } - if lhsNameOrder != rhsNameOrder { - return false - } - if lhsEditing != rhsEditing { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - return true - } else { + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs { + if lhsIndex != rhsIndex { return false } - case let .addItem(lhsTheme, lhsText, lhsEditing): - if case let .addItem(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { - return true - } else { + if lhsPeer != rhsPeer { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsDateTimeFormat != rhsDateTimeFormat { + return false + } + if lhsNameOrder != rhsNameOrder { + return false + } + if lhsEditing != rhsEditing { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + case let .addItem(lhsTheme, lhsText, lhsEditing): + if case let .addItem(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { + return true + } else { + return false + } + case let .headerItem(lhsTheme, lhsText): + if case let .headerItem(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .deleteItem(lhsTheme, lhsText): + if case let .deleteItem(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } static func <(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { switch lhs { + case .deleteItem: + return false case let .peerItem(index, _, _, _, _, _, _, _): switch rhs { + case .deleteItem: + return true case let .peerItem(rhsIndex, _, _, _, _, _, _, _): return index < rhsIndex - case .addItem: + case .addItem, .headerItem: return false } case .addItem: switch rhs { - case .peerItem: + case .peerItem, .deleteItem: return true + case .headerItem: + return false default: return false } + case .headerItem: + return true } } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! SelectivePrivacyPeersControllerArguments switch self { - case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): - var text: ItemListPeerItemText = .none - if let group = peer.peer as? TelegramGroup { - text = .text(strings.Conversation_StatusMembers(Int32(group.participantCount)), .secondary) - } else if let channel = peer.peer as? TelegramChannel { - if let participantCount = peer.participantCount { - text = .text(strings.Conversation_StatusMembers(Int32(participantCount)), .secondary) - } else { - switch channel.info { - case .group: - text = .text(strings.Group_Status, .secondary) - case .broadcast: - text = .text(strings.Channel_Status, .secondary) - } + case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled): + var text: ItemListPeerItemText = .none + if let group = peer.peer as? TelegramGroup { + text = .text(strings.Conversation_StatusMembers(Int32(group.participantCount)), .secondary) + } else if let channel = peer.peer as? TelegramChannel { + if let participantCount = peer.participantCount { + text = .text(strings.Conversation_StatusMembers(Int32(participantCount)), .secondary) + } else { + switch channel.info { + case .group: + text = .text(strings.Group_Status, .secondary) + case .broadcast: + text = .text(strings.Channel_Status, .secondary) } } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { - arguments.openPeer(EnginePeer(peer.peer)) - }, setPeerIdWithRevealedOptions: { previousId, id in - arguments.setPeerIdWithRevealedOptions(previousId, id) - }, removePeer: { peerId in - arguments.removePeer(peerId) - }) - case let .addItem(theme, text, editing): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { + } + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + arguments.openPeer(EnginePeer(peer.peer)) + }, setPeerIdWithRevealedOptions: { previousId, id in + arguments.setPeerIdWithRevealedOptions(previousId, id) + }, removePeer: { peerId in + arguments.removePeer(peerId) + }) + case let .addItem(theme, text, editing): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, height: .compactPeerList, editing: editing, action: { arguments.addPeer() }) + case let .headerItem(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .deleteItem(_, text): + return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + arguments.deleteAll() + }) } } } @@ -191,6 +227,13 @@ private struct SelectivePrivacyPeersControllerState: Equatable { private func selectivePrivacyPeersControllerEntries(presentationData: PresentationData, state: SelectivePrivacyPeersControllerState, peers: [SelectivePrivacyPeer]) -> [SelectivePrivacyPeersEntry] { var entries: [SelectivePrivacyPeersEntry] = [] + let title: String + if peers.isEmpty { + title = presentationData.strings.Privacy_Exceptions + } else { + title = presentationData.strings.Privacy_ExceptionsCount(Int32(peers.count)) + } + entries.append(.headerItem(presentationData.theme, title)) entries.append(.addItem(presentationData.theme, presentationData.strings.Privacy_AddNewPeer, state.editing)) var index: Int32 = 0 @@ -199,6 +242,10 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati index += 1 } + if !peers.isEmpty { + entries.append(.deleteItem(presentationData.theme, presentationData.strings.Privacy_Exceptions_DeleteAllExceptions)) + } + return entries } @@ -328,6 +375,34 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri return } pushControllerImpl?(controller) + }, deleteAll: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.Privacy_Exceptions_DeleteAllConfirmation), + ActionSheetButtonItem(title: presentationData.strings.Privacy_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let applyPeers: Signal = peersPromise.get() + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { _ -> Signal in + peersPromise.set(.single([])) + updated([:]) + + dismissImpl?() + + return .complete() + } + + removePeerDisposable.set(applyPeers.start()) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) }) var previousPeers: [SelectivePrivacyPeer]? diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 77eceee8e17..fd4e23be838 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -699,13 +699,6 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati return entries } -public enum InstalledStickerPacksControllerMode { - case general - case modal - case masks - case emoji -} - public func installedStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, archivedPacks: [ArchivedStickerPackItem]? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void = { _ in }, focusOnItemTag: InstalledStickerPacksEntryTag? = nil) -> ViewController { let initialState = InstalledStickerPacksControllerState().withUpdatedEditing(mode == .modal).withUpdatedSelectedPackIds(mode == .modal ? Set() : nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) diff --git a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift b/submodules/SettingsUI/Sources/ThemeCarouselItem.swift index 9a54263bb5a..a03e7dbd829 100644 --- a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift +++ b/submodules/SettingsUI/Sources/ThemeCarouselItem.swift @@ -431,7 +431,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index 776f0b226d4..9aee46a7aed 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -1230,7 +1230,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings) |> mapToSignal { cachedWallpaper in if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { - let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() + let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() return .single(wallpaper) diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index 9f0b344c822..54fd2d9affe 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -272,7 +272,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = true - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift index 9f2c5ca0454..901dccdeb07 100644 --- a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift +++ b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift @@ -174,7 +174,7 @@ func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryE let apply: () -> Void = { let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil) - let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)], settings) + let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) updateWallpaper(wallpaper) DispatchQueue.main.async { completion() diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index 801b5294157..0277d47a254 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -407,7 +407,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll |> mapToSignal { wallpaper -> Signal in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal in guard complete, let fullSizeData = fullSizeData else { diff --git a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift index abec3b69990..c2e06860238 100644 --- a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift +++ b/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift @@ -211,7 +211,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { } let fullDimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000) - let convertedFullRepresentations = [ImageRepresentationWithReference(representation: .init(dimensions: fullDimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))] + let convertedFullRepresentations = [ImageRepresentationWithReference(representation: .init(dimensions: fullDimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))] let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> if wallpaper.isPattern { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index d6281ac6b58..1fda96884ff 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -473,7 +473,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate for representation in file.file.previewRepresentations { convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource))) } - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) } else if backgroundColors.count >= 2 { wallpaper = .gradient(TelegramWallpaper.Gradient(id: nil, colors: backgroundColors.map { $0.rgb }, settings: WallpaperSettings(rotation: state.rotation))) } else { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift index e10768ddbf3..9a674220e09 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift @@ -539,7 +539,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue())) |> then( - requestContextResults(context: context, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) + requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) |> map { results -> ChatContextResultCollection? in return results?.results } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift index 8a7e582b4d8..12e53a36dbc 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift @@ -108,14 +108,14 @@ final class ThemeGridSearchItemNode: GridItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } if !representations.isEmpty { let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512)) + updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512)) } else { updateImageSignal = .complete() } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index f963f864cff..c1724ac735f 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -252,7 +252,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { for representation in file.file.previewRepresentations { convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource))) } - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> if wallpaper.isPattern { @@ -262,7 +262,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.remoteChatBackgroundNode.setSignal(signal) - strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()) + strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()) let account = strongSelf.context.account let statusSignal = strongSelf.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 7ab7c169d1b..e80a8336a2e 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -1187,7 +1187,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings) |> mapToSignal { cachedWallpaper in if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { - let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() + let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() return .single(wallpaper) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index b619de8b7b1..dc19067e956 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -383,7 +383,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { for representation in file.file.previewRepresentations { convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: representation.resource, media: file.file, message: message, slug: file.slug))) } - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: reference(for: file.file.resource, media: file.file, message: message, slug: file.slug))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: reference(for: file.file.resource, media: file.file, message: message, slug: file.slug))) if wallpaper.isPattern { var patternColors: [UIColor] = [] @@ -422,7 +422,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: fileReference, representations: convertedRepresentations, alwaysShowThumbnailFirst: true, autoFetchFullSize: false) } - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: convertedRepresentations[convertedRepresentations.count - 1].reference) + fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: convertedRepresentations[convertedRepresentations.count - 1].reference) let account = self.context.account statusSignal = self.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource) |> take(1) @@ -452,7 +452,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, alwaysShowThumbnailFirst: true, autoFetchFullSize: false) if let largestIndex = convertedRepresentations.firstIndex(where: { $0.representation == largestSize }) { - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: convertedRepresentations[largestIndex].reference) + fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: convertedRepresentations[largestIndex].reference) } else { fetchSignal = .complete() } @@ -541,13 +541,13 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - signal = chatMessagePhoto(postbox: context.account.postbox, photoReference: .standalone(media: tmpImage)) - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .media(media: .standalone(media: tmpImage), resource: imageResource)) + signal = chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage)) + fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .media(media: .standalone(media: tmpImage), resource: imageResource)) statusSignal = context.account.postbox.mediaBox.resourceStatus(imageResource) } else { displaySize = CGSize(width: 1.0, height: 1.0) diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 06690ef71cd..3200273c18b 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -89,7 +89,7 @@ private enum ExternalShareResourceStatus { private func collectExternalShareResource(postbox: Postbox, resourceReference: MediaResourceReference, statsCategory: MediaResourceStatsCategory) -> Signal { return Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resourceReference, statsCategory: statsCategory).start() + let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resourceReference, statsCategory: statsCategory).start() let data = postbox.mediaBox.resourceData(resourceReference.resource, option: .complete(waitUntilFetchStatus: false)).start(next: { value in if value.complete { subscriber.putNext(.done(value)) @@ -142,7 +142,7 @@ private func collectExternalShareItems(strings: PresentationStrings, dateTimeFor return .single(.progress) case let .done(data): if file.isSticker, !file.isAnimatedSticker, let dimensions = file.dimensions { - return chatMessageSticker(postbox: postbox, file: file, small: false, fetched: true, onlyFullSize: true) + return chatMessageSticker(postbox: postbox, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true) |> map { f -> ExternalShareItemStatus in let context = f(TransformImageArguments(corners: ImageCorners(), imageSize: dimensions.cgSize, boundingSize: dimensions.cgSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, scale: 1.0)) if let image = context?.generateImage() { @@ -998,7 +998,7 @@ public final class ShareController: ViewController { } else { context = self.sharedContext.makeTempAccountContext(account: self.currentAccount) } - return SaveToCameraRoll.saveToCameraRoll(context: context, postbox: postbox, mediaReference: .message(message: MessageReference(message), media: media)) + return SaveToCameraRoll.saveToCameraRoll(context: context, postbox: postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media)) } else { return nil } @@ -1025,7 +1025,7 @@ public final class ShareController: ViewController { } else { context = self.sharedContext.makeTempAccountContext(account: self.currentAccount) } - self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) } private func saveToCameraRoll(mediaReference: AnyMediaReference) { @@ -1035,7 +1035,7 @@ public final class ShareController: ViewController { } else { context = self.sharedContext.makeTempAccountContext(account: self.currentAccount) } - self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: true, completion: {}) + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: true, completion: {}) } private func switchToAccount(account: Account, animateIn: Bool) { diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 7c9589592b0..f4e07f5ef32 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -254,7 +254,7 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])]) - let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black) + let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black) let videoNode = UniversalVideoNode(postbox: account.postbox, audioSession: sharedContext.mediaManager.audioSession, manager: sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index 0e7d528b9fb..7353213b807 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -37,7 +37,7 @@ public final class SoftwareVideoLayerFrameManager { private var didStart = false public var started: () -> Void = { } - public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) { + public init(account: Account, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) { var resource = fileReference.media.resource var secondaryResource: MediaResource? for attribute in fileReference.media.attributes { @@ -59,7 +59,7 @@ public final class SoftwareVideoLayerFrameManager { self.layer = layer ?? layerHolder?.layer self.layer?.videoGravity = .resizeAspectFill self.layer?.masksToBounds = true - self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource)).start() + self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(resource)).start() } deinit { diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 2a8aa7db57a..5322b390837 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -765,6 +765,14 @@ public final class SolidRoundedButtonView: UIView { } } + public var label: String? { + didSet { + if let width = self.validLayout { + _ = self.updateLayout(width: width, transition: .immediate) + } + } + } + public var subtitle: String? { didSet { if let width = self.validLayout { @@ -779,6 +787,14 @@ public final class SolidRoundedButtonView: UIView { } } + public var isEnabled: Bool = true { + didSet { + if self.isEnabled != oldValue { + self.titleNode.alpha = self.isEnabled ? 1.0 : 0.6 + } + } + } + private var animationTimer: SwiftSignalKit.Timer? public var animation: String? { didSet { @@ -854,13 +870,14 @@ public final class SolidRoundedButtonView: UIView { public var progressType: SolidRoundedButtonProgressType = .fullSize - public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { + public init(title: String? = nil, label: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.font = font self.fontSize = fontSize self.buttonHeight = height self.buttonCornerRadius = cornerRadius self.title = title + self.label = label self.gloss = gloss self.buttonBackgroundNode = UIImageView() @@ -1174,7 +1191,13 @@ public final class SolidRoundedButtonView: UIView { self.buttonBackgroundAnimationView?.image = nil } - self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor) + let titleText = NSMutableAttributedString() + titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)) + if let label = self.label { + titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6))) + } + + self.titleNode.attributedText = titleText self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor) @@ -1219,10 +1242,14 @@ public final class SolidRoundedButtonView: UIView { transition.updateFrame(view: self.buttonNode, frame: buttonFrame) - if self.title != self.titleNode.attributedText?.string { - self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor) + let titleText = NSMutableAttributedString() + titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)) + if let label = self.label { + titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6))) } + self.titleNode.attributedText = titleText + let iconSize: CGSize if let _ = self.animationNode { iconSize = CGSize(width: 30.0, height: 30.0) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 406b1cd4e33..0623f9b1343 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -1159,10 +1159,18 @@ public final class SparseItemGrid: ASDisplayNode { self.coveringOffsetUpdated = coveringOffsetUpdated super.init() + + self.fromViewport.allowsGroupOpacity = true + self.toViewport.allowsGroupOpacity = true self.addSubnode(fromViewport) self.addSubnode(toViewport) } + + deinit { + self.fromViewport.allowsGroupOpacity = false + self.toViewport.allowsGroupOpacity = false + } func update(progress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { guard var fromAnchorFrame = self.fromViewport.frameForItem(at: self.anchorItemIndex) else { diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index a3725e2a84d..b76ff08fa2b 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -277,9 +277,9 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { if let currentContentImageMedia = currentContentImageMedia, contentImageMedia.isSemanticallyEqual(to: currentContentImageMedia) { } else { if let image = contentImageMedia as? TelegramMediaImage { - updateImageSignal = mediaGridMessagePhoto(account: item.context.account, photoReference: .message(message: MessageReference(item.message), media: image)) + updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image)) } else if let file = contentImageMedia as? TelegramMediaFile { - updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), autoFetchFullSizeThumbnail: true) + updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), autoFetchFullSizeThumbnail: true) } } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index 98eca08ff26..60c04718966 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -203,7 +203,7 @@ final class StickerPackEmojisItemNode: GridItemNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop @@ -229,7 +229,7 @@ final class StickerPackEmojisItemNode: GridItemNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop @@ -354,7 +354,7 @@ final class StickerPackEmojisItemNode: GridItemNode { } else { updateItemLayerPlaceholder = true itemTransition = .immediate - + let animationData = EntityKeyboardAnimationData(file: item.file) itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( @@ -363,7 +363,7 @@ final class StickerPackEmojisItemNode: GridItemNode { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ), context: context, attemptSynchronousLoad: attemptSynchronousLoads, @@ -425,6 +425,15 @@ final class StickerPackEmojisItemNode: GridItemNode { self.visibleItemLayers[itemId] = itemLayer } + switch itemLayer.item.tintMode { + case .none: + break + case .accent: + itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor + case .primary: + itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor + } + var itemFrame = itemLayout.frame(itemIndex: index) itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 5e3e66a6c6b..3b56f2e42e1 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -188,7 +188,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese if let thumbnail = info.thumbnail { let signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() let data = account.postbox.mediaBox.resourceData(thumbnail.resource, option: .incremental(waitUntilFetchStatus: false)).start(next: { data in if data.complete { subscriber.putNext(true) @@ -209,10 +209,10 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese for item in topItems { if item.file.isAnimatedSticker { let signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: FileMediaReference.standalone(media: item.file).resourceReference(item.file.resource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: FileMediaReference.standalone(media: item.file).resourceReference(item.file.resource)).start() let data = account.postbox.mediaBox.resourceData(item.file.resource).start() let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, file: item.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in + let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, userLocation: .other, file: item.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in let hasContent = next._0 != nil || next._1 != nil subscriber.putNext(hasContent) if hasContent { @@ -289,7 +289,7 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { if let thumbnail = info.thumbnail { let signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)).start() let dataDisposable: Disposable if info.flags.contains(.isAnimated) || info.flags.contains(.isVideo) { dataDisposable = chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: thumbnail.resource, isVideo: info.flags.contains(.isVideo), width: 80, height: 80, synchronousLoad: false).start(next: { data in @@ -321,10 +321,10 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol if let item = items.first as? StickerPackItem { if item.file.isAnimatedSticker { let signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: FileMediaReference.standalone(media: item.file).resourceReference(item.file.resource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: FileMediaReference.standalone(media: item.file).resourceReference(item.file.resource)).start() let data = account.postbox.mediaBox.resourceData(item.file.resource).start() let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, file: item.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in + let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, userLocation: .other, file: item.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in let hasContent = next._0 != nil || next._1 != nil subscriber.putNext(hasContent) if hasContent { @@ -342,7 +342,7 @@ public func preloadedStickerPackThumbnail(account: Account, info: StickerPackCol let signal = Signal { subscriber in let data = account.postbox.mediaBox.resourceData(item.file.resource).start() let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) - let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, file: item.file, small: true, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in + let fetchedRepresentation = chatMessageAnimatedStickerDatas(postbox: account.postbox, userLocation: .other, file: item.file, small: true, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)), fetched: true, onlyFullSize: false, synchronousLoad: false).start(next: { next in let hasContent = next._0 != nil || next._1 != nil subscriber.putNext(hasContent) if hasContent { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index 8849783d9b1..b081b5cf5b8 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -229,9 +229,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode { if stickerItem.file.isAnimatedSticker || stickerItem.file.isVideoSticker { let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) if stickerItem.file.isVideoSticker { - self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: stickerItem.file, small: true)) } else { - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, userLocation: .other, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) } if self.animationNode == nil { @@ -259,10 +259,10 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.animationNode?.visibility = visibility - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) if stickerItem.file.isPremiumSticker, let effect = stickerItem.file.videoThumbnails.first { - self.effectFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: effect.resource).start()) + self.effectFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(stickerItem.file), resource: effect.resource).start()) } } else { if let animationNode = self.animationNode { @@ -270,8 +270,8 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.animationNode = nil animationNode.removeFromSupernode() } - self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) } } else { if isEmpty { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 0808de4c7d9..934129b7f5b 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -66,7 +66,7 @@ private struct StickerPackPreviewGridTransaction { let scrollToItem: GridNodeScrollToItem? init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], context: AccountContext, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, scrollToItem: GridNodeScrollToItem?) { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings, animationCache: animationCache, animationRenderer: animationRenderer), previousIndex: $0.2) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift index a0f91ddc6f2..66850c8f93f 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift @@ -137,7 +137,7 @@ final class StickerPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) break } - self.imageNode.setSignal(chatMessageSticker(account: context.account, file: item.file, small: false, onlyFullSize: false)) + self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: item.file, small: false, onlyFullSize: false)) if let (layout, navigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) diff --git a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift index 1eeef0f26ad..088e5b64431 100644 --- a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift @@ -130,7 +130,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC animationNode.addSubnode(self.textNode) if isPremiumSticker, let effect = item.file.videoThumbnails.first { - self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: .standalone(media: item.file), resource: effect.resource).start()) + self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: .standalone(media: item.file), resource: effect.resource).start()) let source = AnimatedStickerResourceSource(account: account, resource: effect.resource, fitzModifier: nil) let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() @@ -143,7 +143,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.animationNode = nil } - self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: item.file, small: false, fetched: true)) super.init() diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 4e89fa8e744..815840466bb 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -48,7 +48,7 @@ public func chatMessageStickerResource(file: TelegramMediaFile, small: Bool) -> return resource } -private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile, small: Bool, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool) -> Signal, NoError> { +private func chatMessageStickerDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool) -> Signal, NoError> { let thumbnailResource = chatMessageStickerResource(file: file, small: true) let resource = chatMessageStickerResource(file: file, small: small) @@ -71,12 +71,12 @@ private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile, return Signal { subscriber in var fetch: Disposable? if fetched { - fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(resource)).start() + fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(resource)).start() } var fetchThumbnail: Disposable? if thumbnailResource.id != resource.id { - fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() + fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() } let disposable = (combineLatest(thumbnailData, fullSizeData) |> map { thumbnailData, fullSizeData -> Tuple3 in @@ -98,7 +98,7 @@ private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile, } } -public func chatMessageAnimatedStickerDatas(postbox: Postbox, file: TelegramMediaFile, small: Bool, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool) -> Signal, NoError> { +public func chatMessageAnimatedStickerDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool) -> Signal, NoError> { let thumbnailResource = chatMessageStickerResource(file: file, small: true) let resource = chatMessageStickerResource(file: file, small: false) @@ -122,12 +122,12 @@ public func chatMessageAnimatedStickerDatas(postbox: Postbox, file: TelegramMedi return Signal { subscriber in var fetch: Disposable? if fetched { - fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(resource)).start() + fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(resource)).start() } var fetchThumbnail: Disposable? if thumbnailResource.id != resource.id { - fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() + fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() } let disposable = (combineLatest(thumbnailData, fullSizeData) |> map { thumbnailData, fullSizeData -> Tuple3 in @@ -149,7 +149,7 @@ public func chatMessageAnimatedStickerDatas(postbox: Postbox, file: TelegramMedi } } -private func chatMessageStickerThumbnailData(postbox: Postbox, file: TelegramMediaFile, synchronousLoad: Bool) -> Signal { +private func chatMessageStickerThumbnailData(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, synchronousLoad: Bool) -> Signal { let thumbnailResource = chatMessageStickerResource(file: file, small: true) let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedStickerAJpegRepresentation(size: nil), complete: false, fetch: false, attemptSynchronously: synchronousLoad) @@ -164,7 +164,7 @@ private func chatMessageStickerThumbnailData(postbox: Postbox, file: TelegramMed let thumbnailData = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedStickerAJpegRepresentation(size: nil), complete: false) return Signal { subscriber in - let fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() + let fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .sticker, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() let disposable = (thumbnailData |> map { thumbnailData -> Data? in @@ -259,8 +259,8 @@ public func chatMessageAnimatedStickerBackingData(postbox: Postbox, fileReferenc } } -public func chatMessageLegacySticker(account: Account, file: TelegramMediaFile, small: Bool, fitSize: CGSize, fetched: Bool = false, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageStickerDatas(postbox: account.postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: false) +public func chatMessageLegacySticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fitSize: CGSize, fetched: Bool = false, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageStickerDatas(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: false) return signal |> map { value in let fullSizeData = value._1 let fullSizeComplete = value._2 @@ -328,8 +328,8 @@ public func chatMessageLegacySticker(account: Account, file: TelegramMediaFile, } } -public func chatMessageSticker(account: Account, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return chatMessageSticker(postbox: account.postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad) +public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad) } public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { @@ -385,15 +385,15 @@ public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaRes } } -public func chatMessageSticker(postbox: Postbox, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { - signal = chatMessageStickerThumbnailData(postbox: postbox, file: file, synchronousLoad: synchronousLoad) + signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad) |> map { data -> Tuple3in return Tuple3(data, nil, false) } } else { - signal = chatMessageStickerDatas(postbox: postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad) + signal = chatMessageStickerDatas(postbox: postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad) } return signal |> map { value in let thumbnailData = value._0 @@ -486,15 +486,15 @@ public func chatMessageSticker(postbox: Postbox, file: TelegramMediaFile, small: } } -public func chatMessageAnimatedSticker(postbox: Postbox, file: TelegramMediaFile, small: Bool, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageAnimatedSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, size: CGSize, fitzModifier: EmojiFitzModifier? = nil, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { - signal = chatMessageStickerThumbnailData(postbox: postbox, file: file, synchronousLoad: synchronousLoad) + signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad) |> map { data -> Tuple3 in return Tuple(data, nil, false) } } else { - signal = chatMessageAnimatedStickerDatas(postbox: postbox, file: file, small: small, size: size, fitzModifier: fitzModifier, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad) + signal = chatMessageAnimatedStickerDatas(postbox: postbox, userLocation: userLocation, file: file, small: small, size: size, fitzModifier: fitzModifier, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad) } return signal |> map { value in diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4df27e6e4a5..f321ea20bf8 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -122,6 +122,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[663693416] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionSendMessage($0) } dict[589338437] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStartGroupCall($0) } dict[-1895328189] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStopPoll($0) } + dict[1693675004] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAntiSpam($0) } dict[46949251] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleForum($0) } dict[1456906823] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleGroupCallSetting($0) } dict[460916654] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleInvites($0) } @@ -426,6 +427,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[940666592] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } + dict[-404267113] = { return Api.MessageAction.parse_messageActionAttachMenuBotAllowed($0) } dict[-1410748418] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) } dict[-365344535] = { return Api.MessageAction.parse_messageActionChannelMigrateFrom($0) } @@ -457,6 +459,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[455635795] = { return Api.MessageAction.parse_messageActionSecureValuesSentMe($0) } dict[-1434950843] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } + dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } dict[-1262252875] = { return Api.MessageAction.parse_messageActionWebViewDataSent($0) } @@ -751,6 +754,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) } dict[1087454222] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } + dict[2008112412] = { return Api.StickerSetCovered.parse_stickerSetNoCovered($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } @@ -864,10 +868,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[196268545] = { return Api.Update.parse_updateStickerSetsOrder($0) } dict[-2112423005] = { return Api.Update.parse_updateTheme($0) } dict[8703322] = { return Api.Update.parse_updateTranscribedAudio($0) } + dict[542282808] = { return Api.Update.parse_updateUser($0) } dict[674706841] = { return Api.Update.parse_updateUserEmojiStatus($0) } dict[-1484486364] = { return Api.Update.parse_updateUserName($0) } dict[88680979] = { return Api.Update.parse_updateUserPhone($0) } - dict[-232290676] = { return Api.Update.parse_updateUserPhoto($0) } dict[-440534818] = { return Api.Update.parse_updateUserStatus($0) } dict[-1071741569] = { return Api.Update.parse_updateUserTyping($0) } dict[2139689491] = { return Api.Update.parse_updateWebPage($0) } @@ -884,7 +888,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[-1885878744] = { return Api.User.parse_user($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) } - dict[-994968513] = { return Api.UserFull.parse_userFull($0) } + dict[-120378643] = { return Api.UserFull.parse_userFull($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) } dict[164646985] = { return Api.UserStatus.parse_userStatusEmpty($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 5718ccf94df..01829ba6dec 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -16,7 +16,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .accountDaysTTL(let days): - return ("accountDaysTTL", [("days", String(describing: days))]) + return ("accountDaysTTL", [("days", days as Any)]) } } @@ -64,7 +64,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .attachMenuBot(let flags, let botId, let shortName, let peerTypes, let icons): - return ("attachMenuBot", [("flags", String(describing: flags)), ("botId", String(describing: botId)), ("shortName", String(describing: shortName)), ("peerTypes", String(describing: peerTypes)), ("icons", String(describing: icons))]) + return ("attachMenuBot", [("flags", flags as Any), ("botId", botId as Any), ("shortName", shortName as Any), ("peerTypes", peerTypes as Any), ("icons", icons as Any)]) } } @@ -123,7 +123,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .attachMenuBotIcon(let flags, let name, let icon, let colors): - return ("attachMenuBotIcon", [("flags", String(describing: flags)), ("name", String(describing: name)), ("icon", String(describing: icon)), ("colors", String(describing: colors))]) + return ("attachMenuBotIcon", [("flags", flags as Any), ("name", name as Any), ("icon", icon as Any), ("colors", colors as Any)]) } } @@ -173,7 +173,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .attachMenuBotIconColor(let name, let color): - return ("attachMenuBotIconColor", [("name", String(describing: name)), ("color", String(describing: color))]) + return ("attachMenuBotIconColor", [("name", name as Any), ("color", color as Any)]) } } @@ -229,7 +229,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .attachMenuBots(let hash, let bots, let users): - return ("attachMenuBots", [("hash", String(describing: hash)), ("bots", String(describing: bots)), ("users", String(describing: users))]) + return ("attachMenuBots", [("hash", hash as Any), ("bots", bots as Any), ("users", users as Any)]) case .attachMenuBotsNotModified: return ("attachMenuBotsNotModified", []) } @@ -285,7 +285,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .attachMenuBotsBot(let bot, let users): - return ("attachMenuBotsBot", [("bot", String(describing: bot)), ("users", String(describing: users))]) + return ("attachMenuBotsBot", [("bot", bot as Any), ("users", users as Any)]) } } @@ -416,7 +416,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .authorization(let flags, let hash, let deviceModel, let platform, let systemVersion, let apiId, let appName, let appVersion, let dateCreated, let dateActive, let ip, let country, let region): - return ("authorization", [("flags", String(describing: flags)), ("hash", String(describing: hash)), ("deviceModel", String(describing: deviceModel)), ("platform", String(describing: platform)), ("systemVersion", String(describing: systemVersion)), ("apiId", String(describing: apiId)), ("appName", String(describing: appName)), ("appVersion", String(describing: appVersion)), ("dateCreated", String(describing: dateCreated)), ("dateActive", String(describing: dateActive)), ("ip", String(describing: ip)), ("country", String(describing: country)), ("region", String(describing: region))]) + return ("authorization", [("flags", flags as Any), ("hash", hash as Any), ("deviceModel", deviceModel as Any), ("platform", platform as Any), ("systemVersion", systemVersion as Any), ("apiId", apiId as Any), ("appName", appName as Any), ("appVersion", appVersion as Any), ("dateCreated", dateCreated as Any), ("dateActive", dateActive as Any), ("ip", ip as Any), ("country", country as Any), ("region", region as Any)]) } } @@ -492,7 +492,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .autoDownloadSettings(let flags, let photoSizeMax, let videoSizeMax, let fileSizeMax, let videoUploadMaxbitrate): - return ("autoDownloadSettings", [("flags", String(describing: flags)), ("photoSizeMax", String(describing: photoSizeMax)), ("videoSizeMax", String(describing: videoSizeMax)), ("fileSizeMax", String(describing: fileSizeMax)), ("videoUploadMaxbitrate", String(describing: videoUploadMaxbitrate))]) + return ("autoDownloadSettings", [("flags", flags as Any), ("photoSizeMax", photoSizeMax as Any), ("videoSizeMax", videoSizeMax as Any), ("fileSizeMax", fileSizeMax as Any), ("videoUploadMaxbitrate", videoUploadMaxbitrate as Any)]) } } @@ -549,7 +549,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation, let aroundAnimation, let centerIcon): - return ("availableReaction", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("title", String(describing: title)), ("staticIcon", String(describing: staticIcon)), ("appearAnimation", String(describing: appearAnimation)), ("selectAnimation", String(describing: selectAnimation)), ("activateAnimation", String(describing: activateAnimation)), ("effectAnimation", String(describing: effectAnimation)), ("aroundAnimation", String(describing: aroundAnimation)), ("centerIcon", String(describing: centerIcon))]) + return ("availableReaction", [("flags", flags as Any), ("reaction", reaction as Any), ("title", title as Any), ("staticIcon", staticIcon as Any), ("appearAnimation", appearAnimation as Any), ("selectAnimation", selectAnimation as Any), ("activateAnimation", activateAnimation as Any), ("effectAnimation", effectAnimation as Any), ("aroundAnimation", aroundAnimation as Any), ("centerIcon", centerIcon as Any)]) } } @@ -627,7 +627,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .bankCardOpenUrl(let url, let name): - return ("bankCardOpenUrl", [("url", String(describing: url)), ("name", String(describing: name))]) + return ("bankCardOpenUrl", [("url", url as Any), ("name", name as Any)]) } } @@ -783,7 +783,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botCommand(let command, let description): - return ("botCommand", [("command", String(describing: command)), ("description", String(describing: description))]) + return ("botCommand", [("command", command as Any), ("description", description as Any)]) } } @@ -871,11 +871,11 @@ public extension Api { case .botCommandScopeDefault: return ("botCommandScopeDefault", []) case .botCommandScopePeer(let peer): - return ("botCommandScopePeer", [("peer", String(describing: peer))]) + return ("botCommandScopePeer", [("peer", peer as Any)]) case .botCommandScopePeerAdmins(let peer): - return ("botCommandScopePeerAdmins", [("peer", String(describing: peer))]) + return ("botCommandScopePeerAdmins", [("peer", peer as Any)]) case .botCommandScopePeerUser(let peer, let userId): - return ("botCommandScopePeerUser", [("peer", String(describing: peer)), ("userId", String(describing: userId))]) + return ("botCommandScopePeerUser", [("peer", peer as Any), ("userId", userId as Any)]) case .botCommandScopeUsers: return ("botCommandScopeUsers", []) } @@ -968,7 +968,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton): - return ("botInfo", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("description", String(describing: description)), ("descriptionPhoto", String(describing: descriptionPhoto)), ("descriptionDocument", String(describing: descriptionDocument)), ("commands", String(describing: commands)), ("menuButton", String(describing: menuButton))]) + return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 5d867ef90df..ffdf3f84779 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -84,7 +84,7 @@ public extension Api { case .inputStickerSetAnimatedEmojiAnimations: return ("inputStickerSetAnimatedEmojiAnimations", []) case .inputStickerSetDice(let emoticon): - return ("inputStickerSetDice", [("emoticon", String(describing: emoticon))]) + return ("inputStickerSetDice", [("emoticon", emoticon as Any)]) case .inputStickerSetEmojiDefaultStatuses: return ("inputStickerSetEmojiDefaultStatuses", []) case .inputStickerSetEmojiDefaultTopicIcons: @@ -94,11 +94,11 @@ public extension Api { case .inputStickerSetEmpty: return ("inputStickerSetEmpty", []) case .inputStickerSetID(let id, let accessHash): - return ("inputStickerSetID", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputStickerSetID", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputStickerSetPremiumGifts: return ("inputStickerSetPremiumGifts", []) case .inputStickerSetShortName(let shortName): - return ("inputStickerSetShortName", [("shortName", String(describing: shortName))]) + return ("inputStickerSetShortName", [("shortName", shortName as Any)]) } } @@ -183,7 +183,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputStickerSetItem(let flags, let document, let emoji, let maskCoords): - return ("inputStickerSetItem", [("flags", String(describing: flags)), ("document", String(describing: document)), ("emoji", String(describing: emoji)), ("maskCoords", String(describing: maskCoords))]) + return ("inputStickerSetItem", [("flags", flags as Any), ("document", document as Any), ("emoji", emoji as Any), ("maskCoords", maskCoords as Any)]) } } @@ -239,9 +239,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputStickeredMediaDocument(let id): - return ("inputStickeredMediaDocument", [("id", String(describing: id))]) + return ("inputStickeredMediaDocument", [("id", id as Any)]) case .inputStickeredMediaPhoto(let id): - return ("inputStickeredMediaPhoto", [("id", String(describing: id))]) + return ("inputStickeredMediaPhoto", [("id", id as Any)]) } } @@ -301,9 +301,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputStorePaymentGiftPremium(let userId, let currency, let amount): - return ("inputStorePaymentGiftPremium", [("userId", String(describing: userId)), ("currency", String(describing: currency)), ("amount", String(describing: amount))]) + return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): - return ("inputStorePaymentPremiumSubscription", [("flags", String(describing: flags))]) + return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)]) } } @@ -366,9 +366,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputTheme(let id, let accessHash): - return ("inputTheme", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputTheme", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputThemeSlug(let slug): - return ("inputThemeSlug", [("slug", String(describing: slug))]) + return ("inputThemeSlug", [("slug", slug as Any)]) } } @@ -428,7 +428,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputThemeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper, let wallpaperSettings): - return ("inputThemeSettings", [("flags", String(describing: flags)), ("baseTheme", String(describing: baseTheme)), ("accentColor", String(describing: accentColor)), ("outboxAccentColor", String(describing: outboxAccentColor)), ("messageColors", String(describing: messageColors)), ("wallpaper", String(describing: wallpaper)), ("wallpaperSettings", String(describing: wallpaperSettings))]) + return ("inputThemeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any), ("wallpaperSettings", wallpaperSettings as Any)]) } } @@ -514,11 +514,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputUser(let userId, let accessHash): - return ("inputUser", [("userId", String(describing: userId)), ("accessHash", String(describing: accessHash))]) + return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) case .inputUserEmpty: return ("inputUserEmpty", []) case .inputUserFromMessage(let peer, let msgId, let userId): - return ("inputUserFromMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("userId", String(describing: userId))]) + return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) case .inputUserSelf: return ("inputUserSelf", []) } @@ -599,11 +599,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputWallPaper(let id, let accessHash): - return ("inputWallPaper", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputWallPaperNoFile(let id): - return ("inputWallPaperNoFile", [("id", String(describing: id))]) + return ("inputWallPaperNoFile", [("id", id as Any)]) case .inputWallPaperSlug(let slug): - return ("inputWallPaperSlug", [("slug", String(describing: slug))]) + return ("inputWallPaperSlug", [("slug", slug as Any)]) } } @@ -671,7 +671,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputWebDocument(let url, let size, let mimeType, let attributes): - return ("inputWebDocument", [("url", String(describing: url)), ("size", String(describing: size)), ("mimeType", String(describing: mimeType)), ("attributes", String(describing: attributes))]) + return ("inputWebDocument", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) } } @@ -741,11 +741,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputWebFileAudioAlbumThumbLocation(let flags, let document, let title, let performer): - return ("inputWebFileAudioAlbumThumbLocation", [("flags", String(describing: flags)), ("document", String(describing: document)), ("title", String(describing: title)), ("performer", String(describing: performer))]) + return ("inputWebFileAudioAlbumThumbLocation", [("flags", flags as Any), ("document", document as Any), ("title", title as Any), ("performer", performer as Any)]) case .inputWebFileGeoPointLocation(let geoPoint, let accessHash, let w, let h, let zoom, let scale): - return ("inputWebFileGeoPointLocation", [("geoPoint", String(describing: geoPoint)), ("accessHash", String(describing: accessHash)), ("w", String(describing: w)), ("h", String(describing: h)), ("zoom", String(describing: zoom)), ("scale", String(describing: scale))]) + return ("inputWebFileGeoPointLocation", [("geoPoint", geoPoint as Any), ("accessHash", accessHash as Any), ("w", w as Any), ("h", h as Any), ("zoom", zoom as Any), ("scale", scale as Any)]) case .inputWebFileLocation(let url, let accessHash): - return ("inputWebFileLocation", [("url", String(describing: url)), ("accessHash", String(describing: accessHash))]) + return ("inputWebFileLocation", [("url", url as Any), ("accessHash", accessHash as Any)]) } } @@ -847,7 +847,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .invoice(let flags, let currency, let prices, let maxTipAmount, let suggestedTipAmounts, let recurringTermsUrl): - return ("invoice", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("prices", String(describing: prices)), ("maxTipAmount", String(describing: maxTipAmount)), ("suggestedTipAmounts", String(describing: suggestedTipAmounts)), ("recurringTermsUrl", String(describing: recurringTermsUrl))]) + return ("invoice", [("flags", flags as Any), ("currency", currency as Any), ("prices", prices as Any), ("maxTipAmount", maxTipAmount as Any), ("suggestedTipAmounts", suggestedTipAmounts as Any), ("recurringTermsUrl", recurringTermsUrl as Any)]) } } @@ -903,7 +903,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .jsonObjectValue(let key, let value): - return ("jsonObjectValue", [("key", String(describing: key)), ("value", String(describing: value))]) + return ("jsonObjectValue", [("key", key as Any), ("value", value as Any)]) } } @@ -987,17 +987,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .jsonArray(let value): - return ("jsonArray", [("value", String(describing: value))]) + return ("jsonArray", [("value", value as Any)]) case .jsonBool(let value): - return ("jsonBool", [("value", String(describing: value))]) + return ("jsonBool", [("value", value as Any)]) case .jsonNull: return ("jsonNull", []) case .jsonNumber(let value): - return ("jsonNumber", [("value", String(describing: value))]) + return ("jsonNumber", [("value", value as Any)]) case .jsonObject(let value): - return ("jsonObject", [("value", String(describing: value))]) + return ("jsonObject", [("value", value as Any)]) case .jsonString(let value): - return ("jsonString", [("value", String(describing: value))]) + return ("jsonString", [("value", value as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index f2b27273ff2..b3a214f317a 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -133,35 +133,35 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputKeyboardButtonUrlAuth(let flags, let text, let fwdText, let url, let bot): - return ("inputKeyboardButtonUrlAuth", [("flags", String(describing: flags)), ("text", String(describing: text)), ("fwdText", String(describing: fwdText)), ("url", String(describing: url)), ("bot", String(describing: bot))]) + return ("inputKeyboardButtonUrlAuth", [("flags", flags as Any), ("text", text as Any), ("fwdText", fwdText as Any), ("url", url as Any), ("bot", bot as Any)]) case .inputKeyboardButtonUserProfile(let text, let userId): - return ("inputKeyboardButtonUserProfile", [("text", String(describing: text)), ("userId", String(describing: userId))]) + return ("inputKeyboardButtonUserProfile", [("text", text as Any), ("userId", userId as Any)]) case .keyboardButton(let text): - return ("keyboardButton", [("text", String(describing: text))]) + return ("keyboardButton", [("text", text as Any)]) case .keyboardButtonBuy(let text): - return ("keyboardButtonBuy", [("text", String(describing: text))]) + return ("keyboardButtonBuy", [("text", text as Any)]) case .keyboardButtonCallback(let flags, let text, let data): - return ("keyboardButtonCallback", [("flags", String(describing: flags)), ("text", String(describing: text)), ("data", String(describing: data))]) + return ("keyboardButtonCallback", [("flags", flags as Any), ("text", text as Any), ("data", data as Any)]) case .keyboardButtonGame(let text): - return ("keyboardButtonGame", [("text", String(describing: text))]) + return ("keyboardButtonGame", [("text", text as Any)]) case .keyboardButtonRequestGeoLocation(let text): - return ("keyboardButtonRequestGeoLocation", [("text", String(describing: text))]) + return ("keyboardButtonRequestGeoLocation", [("text", text as Any)]) case .keyboardButtonRequestPhone(let text): - return ("keyboardButtonRequestPhone", [("text", String(describing: text))]) + return ("keyboardButtonRequestPhone", [("text", text as Any)]) case .keyboardButtonRequestPoll(let flags, let quiz, let text): - return ("keyboardButtonRequestPoll", [("flags", String(describing: flags)), ("quiz", String(describing: quiz)), ("text", String(describing: text))]) + return ("keyboardButtonRequestPoll", [("flags", flags as Any), ("quiz", quiz as Any), ("text", text as Any)]) case .keyboardButtonSimpleWebView(let text, let url): - return ("keyboardButtonSimpleWebView", [("text", String(describing: text)), ("url", String(describing: url))]) + return ("keyboardButtonSimpleWebView", [("text", text as Any), ("url", url as Any)]) case .keyboardButtonSwitchInline(let flags, let text, let query): - return ("keyboardButtonSwitchInline", [("flags", String(describing: flags)), ("text", String(describing: text)), ("query", String(describing: query))]) + return ("keyboardButtonSwitchInline", [("flags", flags as Any), ("text", text as Any), ("query", query as Any)]) case .keyboardButtonUrl(let text, let url): - return ("keyboardButtonUrl", [("text", String(describing: text)), ("url", String(describing: url))]) + return ("keyboardButtonUrl", [("text", text as Any), ("url", url as Any)]) case .keyboardButtonUrlAuth(let flags, let text, let fwdText, let url, let buttonId): - return ("keyboardButtonUrlAuth", [("flags", String(describing: flags)), ("text", String(describing: text)), ("fwdText", String(describing: fwdText)), ("url", String(describing: url)), ("buttonId", String(describing: buttonId))]) + return ("keyboardButtonUrlAuth", [("flags", flags as Any), ("text", text as Any), ("fwdText", fwdText as Any), ("url", url as Any), ("buttonId", buttonId as Any)]) case .keyboardButtonUserProfile(let text, let userId): - return ("keyboardButtonUserProfile", [("text", String(describing: text)), ("userId", String(describing: userId))]) + return ("keyboardButtonUserProfile", [("text", text as Any), ("userId", userId as Any)]) case .keyboardButtonWebView(let text, let url): - return ("keyboardButtonWebView", [("text", String(describing: text)), ("url", String(describing: url))]) + return ("keyboardButtonWebView", [("text", text as Any), ("url", url as Any)]) } } @@ -418,7 +418,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .keyboardButtonRow(let buttons): - return ("keyboardButtonRow", [("buttons", String(describing: buttons))]) + return ("keyboardButtonRow", [("buttons", buttons as Any)]) } } @@ -457,7 +457,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .labeledPrice(let label, let amount): - return ("labeledPrice", [("label", String(describing: label)), ("amount", String(describing: amount))]) + return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) } } @@ -503,7 +503,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .langPackDifference(let langCode, let fromVersion, let version, let strings): - return ("langPackDifference", [("langCode", String(describing: langCode)), ("fromVersion", String(describing: fromVersion)), ("version", String(describing: version)), ("strings", String(describing: strings))]) + return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) } } @@ -558,7 +558,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .langPackLanguage(let flags, let name, let nativeName, let langCode, let baseLangCode, let pluralCode, let stringsCount, let translatedCount, let translationsUrl): - return ("langPackLanguage", [("flags", String(describing: flags)), ("name", String(describing: name)), ("nativeName", String(describing: nativeName)), ("langCode", String(describing: langCode)), ("baseLangCode", String(describing: baseLangCode)), ("pluralCode", String(describing: pluralCode)), ("stringsCount", String(describing: stringsCount)), ("translatedCount", String(describing: translatedCount)), ("translationsUrl", String(describing: translationsUrl))]) + return ("langPackLanguage", [("flags", flags as Any), ("name", name as Any), ("nativeName", nativeName as Any), ("langCode", langCode as Any), ("baseLangCode", baseLangCode as Any), ("pluralCode", pluralCode as Any), ("stringsCount", stringsCount as Any), ("translatedCount", translatedCount as Any), ("translationsUrl", translationsUrl as Any)]) } } @@ -640,11 +640,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .langPackString(let key, let value): - return ("langPackString", [("key", String(describing: key)), ("value", String(describing: value))]) + return ("langPackString", [("key", key as Any), ("value", value as Any)]) case .langPackStringDeleted(let key): - return ("langPackStringDeleted", [("key", String(describing: key))]) + return ("langPackStringDeleted", [("key", key as Any)]) case .langPackStringPluralized(let flags, let key, let zeroValue, let oneValue, let twoValue, let fewValue, let manyValue, let otherValue): - return ("langPackStringPluralized", [("flags", String(describing: flags)), ("key", String(describing: key)), ("zeroValue", String(describing: zeroValue)), ("oneValue", String(describing: oneValue)), ("twoValue", String(describing: twoValue)), ("fewValue", String(describing: fewValue)), ("manyValue", String(describing: manyValue)), ("otherValue", String(describing: otherValue))]) + return ("langPackStringPluralized", [("flags", flags as Any), ("key", key as Any), ("zeroValue", zeroValue as Any), ("oneValue", oneValue as Any), ("twoValue", twoValue as Any), ("fewValue", fewValue as Any), ("manyValue", manyValue as Any), ("otherValue", otherValue as Any)]) } } @@ -729,7 +729,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .maskCoords(let n, let x, let y, let zoom): - return ("maskCoords", [("n", String(describing: n)), ("x", String(describing: x)), ("y", String(describing: y)), ("zoom", String(describing: zoom))]) + return ("maskCoords", [("n", n as Any), ("x", x as Any), ("y", y as Any), ("zoom", zoom as Any)]) } } @@ -825,11 +825,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): - return ("message", [("flags", String(describing: flags)), ("id", String(describing: id)), ("fromId", String(describing: fromId)), ("peerId", String(describing: peerId)), ("fwdFrom", String(describing: fwdFrom)), ("viaBotId", String(describing: viaBotId)), ("replyTo", String(describing: replyTo)), ("date", String(describing: date)), ("message", String(describing: message)), ("media", String(describing: media)), ("replyMarkup", String(describing: replyMarkup)), ("entities", String(describing: entities)), ("views", String(describing: views)), ("forwards", String(describing: forwards)), ("replies", String(describing: replies)), ("editDate", String(describing: editDate)), ("postAuthor", String(describing: postAuthor)), ("groupedId", String(describing: groupedId)), ("reactions", String(describing: reactions)), ("restrictionReason", String(describing: restrictionReason)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("message", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any)]) case .messageEmpty(let flags, let id, let peerId): - return ("messageEmpty", [("flags", String(describing: flags)), ("id", String(describing: id)), ("peerId", String(describing: peerId))]) + return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): - return ("messageService", [("flags", String(describing: flags)), ("id", String(describing: id)), ("fromId", String(describing: fromId)), ("peerId", String(describing: peerId)), ("replyTo", String(describing: replyTo)), ("date", String(describing: date)), ("action", String(describing: action)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("ttlPeriod", ttlPeriod as Any)]) } } @@ -988,6 +988,7 @@ public extension Api { } public extension Api { enum MessageAction: TypeConstructorDescription { + case messageActionAttachMenuBotAllowed case messageActionBotAllowed(domain: String) case messageActionChannelCreate(title: String) case messageActionChannelMigrateFrom(title: String, chatId: Int64) @@ -1019,6 +1020,7 @@ public extension Api { case messageActionSecureValuesSentMe(values: [Api.SecureValue], credentials: Api.SecureCredentialsEncrypted) case messageActionSetChatTheme(emoticon: String) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) + case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) case messageActionWebViewDataSent(text: String) @@ -1026,6 +1028,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .messageActionAttachMenuBotAllowed: + if boxed { + buffer.appendInt32(-404267113) + } + + break case .messageActionBotAllowed(let domain): if boxed { buffer.appendInt32(-1410748418) @@ -1258,6 +1266,12 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)} break + case .messageActionSuggestProfilePhoto(let photo): + if boxed { + buffer.appendInt32(1474192222) + } + photo.serialize(buffer, true) + break case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): if boxed { buffer.appendInt32(228168278) @@ -1295,79 +1309,86 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .messageActionAttachMenuBotAllowed: + return ("messageActionAttachMenuBotAllowed", []) case .messageActionBotAllowed(let domain): - return ("messageActionBotAllowed", [("domain", String(describing: domain))]) + return ("messageActionBotAllowed", [("domain", domain as Any)]) case .messageActionChannelCreate(let title): - return ("messageActionChannelCreate", [("title", String(describing: title))]) + return ("messageActionChannelCreate", [("title", title as Any)]) case .messageActionChannelMigrateFrom(let title, let chatId): - return ("messageActionChannelMigrateFrom", [("title", String(describing: title)), ("chatId", String(describing: chatId))]) + return ("messageActionChannelMigrateFrom", [("title", title as Any), ("chatId", chatId as Any)]) case .messageActionChatAddUser(let users): - return ("messageActionChatAddUser", [("users", String(describing: users))]) + return ("messageActionChatAddUser", [("users", users as Any)]) case .messageActionChatCreate(let title, let users): - return ("messageActionChatCreate", [("title", String(describing: title)), ("users", String(describing: users))]) + return ("messageActionChatCreate", [("title", title as Any), ("users", users as Any)]) case .messageActionChatDeletePhoto: return ("messageActionChatDeletePhoto", []) case .messageActionChatDeleteUser(let userId): - return ("messageActionChatDeleteUser", [("userId", String(describing: userId))]) + return ("messageActionChatDeleteUser", [("userId", userId as Any)]) case .messageActionChatEditPhoto(let photo): - return ("messageActionChatEditPhoto", [("photo", String(describing: photo))]) + return ("messageActionChatEditPhoto", [("photo", photo as Any)]) case .messageActionChatEditTitle(let title): - return ("messageActionChatEditTitle", [("title", String(describing: title))]) + return ("messageActionChatEditTitle", [("title", title as Any)]) case .messageActionChatJoinedByLink(let inviterId): - return ("messageActionChatJoinedByLink", [("inviterId", String(describing: inviterId))]) + return ("messageActionChatJoinedByLink", [("inviterId", inviterId as Any)]) case .messageActionChatJoinedByRequest: return ("messageActionChatJoinedByRequest", []) case .messageActionChatMigrateTo(let channelId): - return ("messageActionChatMigrateTo", [("channelId", String(describing: channelId))]) + return ("messageActionChatMigrateTo", [("channelId", channelId as Any)]) case .messageActionContactSignUp: return ("messageActionContactSignUp", []) case .messageActionCustomAction(let message): - return ("messageActionCustomAction", [("message", String(describing: message))]) + return ("messageActionCustomAction", [("message", message as Any)]) case .messageActionEmpty: return ("messageActionEmpty", []) case .messageActionGameScore(let gameId, let score): - return ("messageActionGameScore", [("gameId", String(describing: gameId)), ("score", String(describing: score))]) + return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): - return ("messageActionGeoProximityReached", [("fromId", String(describing: fromId)), ("toId", String(describing: toId)), ("distance", String(describing: distance))]) + return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) case .messageActionGiftPremium(let currency, let amount, let months): - return ("messageActionGiftPremium", [("currency", String(describing: currency)), ("amount", String(describing: amount)), ("months", String(describing: months))]) + return ("messageActionGiftPremium", [("currency", currency as Any), ("amount", amount as Any), ("months", months as Any)]) case .messageActionGroupCall(let flags, let call, let duration): - return ("messageActionGroupCall", [("flags", String(describing: flags)), ("call", String(describing: call)), ("duration", String(describing: duration))]) + return ("messageActionGroupCall", [("flags", flags as Any), ("call", call as Any), ("duration", duration as Any)]) case .messageActionGroupCallScheduled(let call, let scheduleDate): - return ("messageActionGroupCallScheduled", [("call", String(describing: call)), ("scheduleDate", String(describing: scheduleDate))]) + return ("messageActionGroupCallScheduled", [("call", call as Any), ("scheduleDate", scheduleDate as Any)]) case .messageActionHistoryClear: return ("messageActionHistoryClear", []) case .messageActionInviteToGroupCall(let call, let users): - return ("messageActionInviteToGroupCall", [("call", String(describing: call)), ("users", String(describing: users))]) + return ("messageActionInviteToGroupCall", [("call", call as Any), ("users", users as Any)]) case .messageActionPaymentSent(let flags, let currency, let totalAmount, let invoiceSlug): - return ("messageActionPaymentSent", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("invoiceSlug", String(describing: invoiceSlug))]) + return ("messageActionPaymentSent", [("flags", flags as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("invoiceSlug", invoiceSlug as Any)]) case .messageActionPaymentSentMe(let flags, let currency, let totalAmount, let payload, let info, let shippingOptionId, let charge): - return ("messageActionPaymentSentMe", [("flags", String(describing: flags)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("payload", String(describing: payload)), ("info", String(describing: info)), ("shippingOptionId", String(describing: shippingOptionId)), ("charge", String(describing: charge))]) + return ("messageActionPaymentSentMe", [("flags", flags as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("payload", payload as Any), ("info", info as Any), ("shippingOptionId", shippingOptionId as Any), ("charge", charge as Any)]) case .messageActionPhoneCall(let flags, let callId, let reason, let duration): - return ("messageActionPhoneCall", [("flags", String(describing: flags)), ("callId", String(describing: callId)), ("reason", String(describing: reason)), ("duration", String(describing: duration))]) + return ("messageActionPhoneCall", [("flags", flags as Any), ("callId", callId as Any), ("reason", reason as Any), ("duration", duration as Any)]) case .messageActionPinMessage: return ("messageActionPinMessage", []) case .messageActionScreenshotTaken: return ("messageActionScreenshotTaken", []) case .messageActionSecureValuesSent(let types): - return ("messageActionSecureValuesSent", [("types", String(describing: types))]) + return ("messageActionSecureValuesSent", [("types", types as Any)]) case .messageActionSecureValuesSentMe(let values, let credentials): - return ("messageActionSecureValuesSentMe", [("values", String(describing: values)), ("credentials", String(describing: credentials))]) + return ("messageActionSecureValuesSentMe", [("values", values as Any), ("credentials", credentials as Any)]) case .messageActionSetChatTheme(let emoticon): - return ("messageActionSetChatTheme", [("emoticon", String(describing: emoticon))]) + return ("messageActionSetChatTheme", [("emoticon", emoticon as Any)]) case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom): - return ("messageActionSetMessagesTTL", [("flags", String(describing: flags)), ("period", String(describing: period)), ("autoSettingFrom", String(describing: autoSettingFrom))]) + return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) + case .messageActionSuggestProfilePhoto(let photo): + return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): - return ("messageActionTopicCreate", [("flags", String(describing: flags)), ("title", String(describing: title)), ("iconColor", String(describing: iconColor)), ("iconEmojiId", String(describing: iconEmojiId))]) + return ("messageActionTopicCreate", [("flags", flags as Any), ("title", title as Any), ("iconColor", iconColor as Any), ("iconEmojiId", iconEmojiId as Any)]) case .messageActionTopicEdit(let flags, let title, let iconEmojiId, let closed, let hidden): - return ("messageActionTopicEdit", [("flags", String(describing: flags)), ("title", String(describing: title)), ("iconEmojiId", String(describing: iconEmojiId)), ("closed", String(describing: closed)), ("hidden", String(describing: hidden))]) + return ("messageActionTopicEdit", [("flags", flags as Any), ("title", title as Any), ("iconEmojiId", iconEmojiId as Any), ("closed", closed as Any), ("hidden", hidden as Any)]) case .messageActionWebViewDataSent(let text): - return ("messageActionWebViewDataSent", [("text", String(describing: text))]) + return ("messageActionWebViewDataSent", [("text", text as Any)]) case .messageActionWebViewDataSentMe(let text, let data): - return ("messageActionWebViewDataSentMe", [("text", String(describing: text)), ("data", String(describing: data))]) + return ("messageActionWebViewDataSentMe", [("text", text as Any), ("data", data as Any)]) } } + public static func parse_messageActionAttachMenuBotAllowed(_ reader: BufferReader) -> MessageAction? { + return Api.MessageAction.messageActionAttachMenuBotAllowed + } public static func parse_messageActionBotAllowed(_ reader: BufferReader) -> MessageAction? { var _1: String? _1 = parseString(reader) @@ -1761,6 +1782,19 @@ public extension Api { return nil } } + public static func parse_messageActionSuggestProfilePhoto(_ reader: BufferReader) -> MessageAction? { + var _1: Api.Photo? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Photo + } + let _c1 = _1 != nil + if _c1 { + return Api.MessageAction.messageActionSuggestProfilePhoto(photo: _1!) + } + else { + return nil + } + } public static func parse_messageActionTopicCreate(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index c3150a81e32..36bae7ef091 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -182,47 +182,47 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputMessageEntityMentionName(let offset, let length, let userId): - return ("inputMessageEntityMentionName", [("offset", String(describing: offset)), ("length", String(describing: length)), ("userId", String(describing: userId))]) + return ("inputMessageEntityMentionName", [("offset", offset as Any), ("length", length as Any), ("userId", userId as Any)]) case .messageEntityBankCard(let offset, let length): - return ("messageEntityBankCard", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityBankCard", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityBlockquote(let offset, let length): - return ("messageEntityBlockquote", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityBlockquote", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityBold(let offset, let length): - return ("messageEntityBold", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityBold", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityBotCommand(let offset, let length): - return ("messageEntityBotCommand", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityBotCommand", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityCashtag(let offset, let length): - return ("messageEntityCashtag", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityCashtag", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityCode(let offset, let length): - return ("messageEntityCode", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityCode", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityCustomEmoji(let offset, let length, let documentId): - return ("messageEntityCustomEmoji", [("offset", String(describing: offset)), ("length", String(describing: length)), ("documentId", String(describing: documentId))]) + return ("messageEntityCustomEmoji", [("offset", offset as Any), ("length", length as Any), ("documentId", documentId as Any)]) case .messageEntityEmail(let offset, let length): - return ("messageEntityEmail", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityEmail", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityHashtag(let offset, let length): - return ("messageEntityHashtag", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityHashtag", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityItalic(let offset, let length): - return ("messageEntityItalic", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityItalic", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityMention(let offset, let length): - return ("messageEntityMention", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityMention", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityMentionName(let offset, let length, let userId): - return ("messageEntityMentionName", [("offset", String(describing: offset)), ("length", String(describing: length)), ("userId", String(describing: userId))]) + return ("messageEntityMentionName", [("offset", offset as Any), ("length", length as Any), ("userId", userId as Any)]) case .messageEntityPhone(let offset, let length): - return ("messageEntityPhone", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityPhone", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityPre(let offset, let length, let language): - return ("messageEntityPre", [("offset", String(describing: offset)), ("length", String(describing: length)), ("language", String(describing: language))]) + return ("messageEntityPre", [("offset", offset as Any), ("length", length as Any), ("language", language as Any)]) case .messageEntitySpoiler(let offset, let length): - return ("messageEntitySpoiler", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntitySpoiler", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityStrike(let offset, let length): - return ("messageEntityStrike", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityStrike", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityTextUrl(let offset, let length, let url): - return ("messageEntityTextUrl", [("offset", String(describing: offset)), ("length", String(describing: length)), ("url", String(describing: url))]) + return ("messageEntityTextUrl", [("offset", offset as Any), ("length", length as Any), ("url", url as Any)]) case .messageEntityUnderline(let offset, let length): - return ("messageEntityUnderline", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityUnderline", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityUnknown(let offset, let length): - return ("messageEntityUnknown", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityUnknown", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityUrl(let offset, let length): - return ("messageEntityUrl", [("offset", String(describing: offset)), ("length", String(describing: length))]) + return ("messageEntityUrl", [("offset", offset as Any), ("length", length as Any)]) } } @@ -569,9 +569,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageExtendedMedia(let media): - return ("messageExtendedMedia", [("media", String(describing: media))]) + return ("messageExtendedMedia", [("media", media as Any)]) case .messageExtendedMediaPreview(let flags, let w, let h, let thumb, let videoDuration): - return ("messageExtendedMediaPreview", [("flags", String(describing: flags)), ("w", String(describing: w)), ("h", String(describing: h)), ("thumb", String(describing: thumb)), ("videoDuration", String(describing: videoDuration))]) + return ("messageExtendedMediaPreview", [("flags", flags as Any), ("w", w as Any), ("h", h as Any), ("thumb", thumb as Any), ("videoDuration", videoDuration as Any)]) } } @@ -642,7 +642,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageFwdHeader(let flags, let fromId, let fromName, let date, let channelPost, let postAuthor, let savedFromPeer, let savedFromMsgId, let psaType): - return ("messageFwdHeader", [("flags", String(describing: flags)), ("fromId", String(describing: fromId)), ("fromName", String(describing: fromName)), ("date", String(describing: date)), ("channelPost", String(describing: channelPost)), ("postAuthor", String(describing: postAuthor)), ("savedFromPeer", String(describing: savedFromPeer)), ("savedFromMsgId", String(describing: savedFromMsgId)), ("psaType", String(describing: psaType))]) + return ("messageFwdHeader", [("flags", flags as Any), ("fromId", fromId as Any), ("fromName", fromName as Any), ("date", date as Any), ("channelPost", channelPost as Any), ("postAuthor", postAuthor as Any), ("savedFromPeer", savedFromPeer as Any), ("savedFromMsgId", savedFromMsgId as Any), ("psaType", psaType as Any)]) } } @@ -708,7 +708,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageInteractionCounters(let msgId, let views, let forwards): - return ("messageInteractionCounters", [("msgId", String(describing: msgId)), ("views", String(describing: views)), ("forwards", String(describing: forwards))]) + return ("messageInteractionCounters", [("msgId", msgId as Any), ("views", views as Any), ("forwards", forwards as Any)]) } } @@ -861,31 +861,31 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageMediaContact(let phoneNumber, let firstName, let lastName, let vcard, let userId): - return ("messageMediaContact", [("phoneNumber", String(describing: phoneNumber)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("vcard", String(describing: vcard)), ("userId", String(describing: userId))]) + return ("messageMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("userId", userId as Any)]) case .messageMediaDice(let value, let emoticon): - return ("messageMediaDice", [("value", String(describing: value)), ("emoticon", String(describing: emoticon))]) + return ("messageMediaDice", [("value", value as Any), ("emoticon", emoticon as Any)]) case .messageMediaDocument(let flags, let document, let ttlSeconds): - return ("messageMediaDocument", [("flags", String(describing: flags)), ("document", String(describing: document)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("messageMediaDocument", [("flags", flags as Any), ("document", document as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaEmpty: return ("messageMediaEmpty", []) case .messageMediaGame(let game): - return ("messageMediaGame", [("game", String(describing: game))]) + return ("messageMediaGame", [("game", game as Any)]) case .messageMediaGeo(let geo): - return ("messageMediaGeo", [("geo", String(describing: geo))]) + return ("messageMediaGeo", [("geo", geo as Any)]) case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius): - return ("messageMediaGeoLive", [("flags", String(describing: flags)), ("geo", String(describing: geo)), ("heading", String(describing: heading)), ("period", String(describing: period)), ("proximityNotificationRadius", String(describing: proximityNotificationRadius))]) + return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)]) case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): - return ("messageMediaInvoice", [("flags", String(describing: flags)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("receiptMsgId", String(describing: receiptMsgId)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("startParam", String(describing: startParam)), ("extendedMedia", String(describing: extendedMedia))]) + return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) case .messageMediaPhoto(let flags, let photo, let ttlSeconds): - return ("messageMediaPhoto", [("flags", String(describing: flags)), ("photo", String(describing: photo)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("messageMediaPhoto", [("flags", flags as Any), ("photo", photo as Any), ("ttlSeconds", ttlSeconds as Any)]) case .messageMediaPoll(let poll, let results): - return ("messageMediaPoll", [("poll", String(describing: poll)), ("results", String(describing: results))]) + return ("messageMediaPoll", [("poll", poll as Any), ("results", results as Any)]) case .messageMediaUnsupported: return ("messageMediaUnsupported", []) case .messageMediaVenue(let geo, let title, let address, let provider, let venueId, let venueType): - return ("messageMediaVenue", [("geo", String(describing: geo)), ("title", String(describing: title)), ("address", String(describing: address)), ("provider", String(describing: provider)), ("venueId", String(describing: venueId)), ("venueType", String(describing: venueType))]) + return ("messageMediaVenue", [("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) case .messageMediaWebPage(let webpage): - return ("messageMediaWebPage", [("webpage", String(describing: webpage))]) + return ("messageMediaWebPage", [("webpage", webpage as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 43bced0e203..01a381dfa40 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -18,7 +18,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messagePeerReaction(let flags, let peerId, let reaction): - return ("messagePeerReaction", [("flags", String(describing: flags)), ("peerId", String(describing: peerId)), ("reaction", String(describing: reaction))]) + return ("messagePeerReaction", [("flags", flags as Any), ("peerId", peerId as Any), ("reaction", reaction as Any)]) } } @@ -65,7 +65,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageRange(let minId, let maxId): - return ("messageRange", [("minId", String(describing: minId)), ("maxId", String(describing: maxId))]) + return ("messageRange", [("minId", minId as Any), ("maxId", maxId as Any)]) } } @@ -114,7 +114,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageReactions(let flags, let results, let recentReactions): - return ("messageReactions", [("flags", String(describing: flags)), ("results", String(describing: results)), ("recentReactions", String(describing: recentReactions))]) + return ("messageReactions", [("flags", flags as Any), ("results", results as Any), ("recentReactions", recentReactions as Any)]) } } @@ -170,7 +170,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageReplies(let flags, let replies, let repliesPts, let recentRepliers, let channelId, let maxId, let readMaxId): - return ("messageReplies", [("flags", String(describing: flags)), ("replies", String(describing: replies)), ("repliesPts", String(describing: repliesPts)), ("recentRepliers", String(describing: recentRepliers)), ("channelId", String(describing: channelId)), ("maxId", String(describing: maxId)), ("readMaxId", String(describing: readMaxId))]) + return ("messageReplies", [("flags", flags as Any), ("replies", replies as Any), ("repliesPts", repliesPts as Any), ("recentRepliers", recentRepliers as Any), ("channelId", channelId as Any), ("maxId", maxId as Any), ("readMaxId", readMaxId as Any)]) } } @@ -229,7 +229,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageReplyHeader(let flags, let replyToMsgId, let replyToPeerId, let replyToTopId): - return ("messageReplyHeader", [("flags", String(describing: flags)), ("replyToMsgId", String(describing: replyToMsgId)), ("replyToPeerId", String(describing: replyToPeerId)), ("replyToTopId", String(describing: replyToTopId))]) + return ("messageReplyHeader", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("replyToTopId", replyToTopId as Any)]) } } @@ -299,11 +299,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageUserVote(let userId, let option, let date): - return ("messageUserVote", [("userId", String(describing: userId)), ("option", String(describing: option)), ("date", String(describing: date))]) + return ("messageUserVote", [("userId", userId as Any), ("option", option as Any), ("date", date as Any)]) case .messageUserVoteInputOption(let userId, let date): - return ("messageUserVoteInputOption", [("userId", String(describing: userId)), ("date", String(describing: date))]) + return ("messageUserVoteInputOption", [("userId", userId as Any), ("date", date as Any)]) case .messageUserVoteMultiple(let userId, let options, let date): - return ("messageUserVoteMultiple", [("userId", String(describing: userId)), ("options", String(describing: options)), ("date", String(describing: date))]) + return ("messageUserVoteMultiple", [("userId", userId as Any), ("options", options as Any), ("date", date as Any)]) } } @@ -381,7 +381,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageViews(let flags, let views, let forwards, let replies): - return ("messageViews", [("flags", String(describing: flags)), ("views", String(describing: views)), ("forwards", String(describing: forwards)), ("replies", String(describing: replies))]) + return ("messageViews", [("flags", flags as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any)]) } } @@ -556,7 +556,7 @@ public extension Api { case .inputMessagesFilterMyMentions: return ("inputMessagesFilterMyMentions", []) case .inputMessagesFilterPhoneCalls(let flags): - return ("inputMessagesFilterPhoneCalls", [("flags", String(describing: flags))]) + return ("inputMessagesFilterPhoneCalls", [("flags", flags as Any)]) case .inputMessagesFilterPhotoVideo: return ("inputMessagesFilterPhotoVideo", []) case .inputMessagesFilterPhotos: @@ -658,7 +658,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .nearestDc(let country, let thisDc, let nearestDc): - return ("nearestDc", [("country", String(describing: country)), ("thisDc", String(describing: thisDc)), ("nearestDc", String(describing: nearestDc))]) + return ("nearestDc", [("country", country as Any), ("thisDc", thisDc as Any), ("nearestDc", nearestDc as Any)]) } } @@ -724,11 +724,11 @@ public extension Api { case .notificationSoundDefault: return ("notificationSoundDefault", []) case .notificationSoundLocal(let title, let data): - return ("notificationSoundLocal", [("title", String(describing: title)), ("data", String(describing: data))]) + return ("notificationSoundLocal", [("title", title as Any), ("data", data as Any)]) case .notificationSoundNone: return ("notificationSoundNone", []) case .notificationSoundRingtone(let id): - return ("notificationSoundRingtone", [("id", String(describing: id))]) + return ("notificationSoundRingtone", [("id", id as Any)]) } } @@ -817,9 +817,9 @@ public extension Api { case .notifyChats: return ("notifyChats", []) case .notifyForumTopic(let peer, let topMsgId): - return ("notifyForumTopic", [("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId))]) + return ("notifyForumTopic", [("peer", peer as Any), ("topMsgId", topMsgId as Any)]) case .notifyPeer(let peer): - return ("notifyPeer", [("peer", String(describing: peer))]) + return ("notifyPeer", [("peer", peer as Any)]) case .notifyUsers: return ("notifyUsers", []) } diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 8bd01dfd9cc..c62dd8720f3 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -33,7 +33,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .page(let flags, let url, let blocks, let photos, let documents, let views): - return ("page", [("flags", String(describing: flags)), ("url", String(describing: url)), ("blocks", String(describing: blocks)), ("photos", String(describing: photos)), ("documents", String(describing: documents)), ("views", String(describing: views))]) + return ("page", [("flags", flags as Any), ("url", url as Any), ("blocks", blocks as Any), ("photos", photos as Any), ("documents", documents as Any), ("views", views as Any)]) } } @@ -352,63 +352,63 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageBlockAnchor(let name): - return ("pageBlockAnchor", [("name", String(describing: name))]) + return ("pageBlockAnchor", [("name", name as Any)]) case .pageBlockAudio(let audioId, let caption): - return ("pageBlockAudio", [("audioId", String(describing: audioId)), ("caption", String(describing: caption))]) + return ("pageBlockAudio", [("audioId", audioId as Any), ("caption", caption as Any)]) case .pageBlockAuthorDate(let author, let publishedDate): - return ("pageBlockAuthorDate", [("author", String(describing: author)), ("publishedDate", String(describing: publishedDate))]) + return ("pageBlockAuthorDate", [("author", author as Any), ("publishedDate", publishedDate as Any)]) case .pageBlockBlockquote(let text, let caption): - return ("pageBlockBlockquote", [("text", String(describing: text)), ("caption", String(describing: caption))]) + return ("pageBlockBlockquote", [("text", text as Any), ("caption", caption as Any)]) case .pageBlockChannel(let channel): - return ("pageBlockChannel", [("channel", String(describing: channel))]) + return ("pageBlockChannel", [("channel", channel as Any)]) case .pageBlockCollage(let items, let caption): - return ("pageBlockCollage", [("items", String(describing: items)), ("caption", String(describing: caption))]) + return ("pageBlockCollage", [("items", items as Any), ("caption", caption as Any)]) case .pageBlockCover(let cover): - return ("pageBlockCover", [("cover", String(describing: cover))]) + return ("pageBlockCover", [("cover", cover as Any)]) case .pageBlockDetails(let flags, let blocks, let title): - return ("pageBlockDetails", [("flags", String(describing: flags)), ("blocks", String(describing: blocks)), ("title", String(describing: title))]) + return ("pageBlockDetails", [("flags", flags as Any), ("blocks", blocks as Any), ("title", title as Any)]) case .pageBlockDivider: return ("pageBlockDivider", []) case .pageBlockEmbed(let flags, let url, let html, let posterPhotoId, let w, let h, let caption): - return ("pageBlockEmbed", [("flags", String(describing: flags)), ("url", String(describing: url)), ("html", String(describing: html)), ("posterPhotoId", String(describing: posterPhotoId)), ("w", String(describing: w)), ("h", String(describing: h)), ("caption", String(describing: caption))]) + return ("pageBlockEmbed", [("flags", flags as Any), ("url", url as Any), ("html", html as Any), ("posterPhotoId", posterPhotoId as Any), ("w", w as Any), ("h", h as Any), ("caption", caption as Any)]) case .pageBlockEmbedPost(let url, let webpageId, let authorPhotoId, let author, let date, let blocks, let caption): - return ("pageBlockEmbedPost", [("url", String(describing: url)), ("webpageId", String(describing: webpageId)), ("authorPhotoId", String(describing: authorPhotoId)), ("author", String(describing: author)), ("date", String(describing: date)), ("blocks", String(describing: blocks)), ("caption", String(describing: caption))]) + return ("pageBlockEmbedPost", [("url", url as Any), ("webpageId", webpageId as Any), ("authorPhotoId", authorPhotoId as Any), ("author", author as Any), ("date", date as Any), ("blocks", blocks as Any), ("caption", caption as Any)]) case .pageBlockFooter(let text): - return ("pageBlockFooter", [("text", String(describing: text))]) + return ("pageBlockFooter", [("text", text as Any)]) case .pageBlockHeader(let text): - return ("pageBlockHeader", [("text", String(describing: text))]) + return ("pageBlockHeader", [("text", text as Any)]) case .pageBlockKicker(let text): - return ("pageBlockKicker", [("text", String(describing: text))]) + return ("pageBlockKicker", [("text", text as Any)]) case .pageBlockList(let items): - return ("pageBlockList", [("items", String(describing: items))]) + return ("pageBlockList", [("items", items as Any)]) case .pageBlockMap(let geo, let zoom, let w, let h, let caption): - return ("pageBlockMap", [("geo", String(describing: geo)), ("zoom", String(describing: zoom)), ("w", String(describing: w)), ("h", String(describing: h)), ("caption", String(describing: caption))]) + return ("pageBlockMap", [("geo", geo as Any), ("zoom", zoom as Any), ("w", w as Any), ("h", h as Any), ("caption", caption as Any)]) case .pageBlockOrderedList(let items): - return ("pageBlockOrderedList", [("items", String(describing: items))]) + return ("pageBlockOrderedList", [("items", items as Any)]) case .pageBlockParagraph(let text): - return ("pageBlockParagraph", [("text", String(describing: text))]) + return ("pageBlockParagraph", [("text", text as Any)]) case .pageBlockPhoto(let flags, let photoId, let caption, let url, let webpageId): - return ("pageBlockPhoto", [("flags", String(describing: flags)), ("photoId", String(describing: photoId)), ("caption", String(describing: caption)), ("url", String(describing: url)), ("webpageId", String(describing: webpageId))]) + return ("pageBlockPhoto", [("flags", flags as Any), ("photoId", photoId as Any), ("caption", caption as Any), ("url", url as Any), ("webpageId", webpageId as Any)]) case .pageBlockPreformatted(let text, let language): - return ("pageBlockPreformatted", [("text", String(describing: text)), ("language", String(describing: language))]) + return ("pageBlockPreformatted", [("text", text as Any), ("language", language as Any)]) case .pageBlockPullquote(let text, let caption): - return ("pageBlockPullquote", [("text", String(describing: text)), ("caption", String(describing: caption))]) + return ("pageBlockPullquote", [("text", text as Any), ("caption", caption as Any)]) case .pageBlockRelatedArticles(let title, let articles): - return ("pageBlockRelatedArticles", [("title", String(describing: title)), ("articles", String(describing: articles))]) + return ("pageBlockRelatedArticles", [("title", title as Any), ("articles", articles as Any)]) case .pageBlockSlideshow(let items, let caption): - return ("pageBlockSlideshow", [("items", String(describing: items)), ("caption", String(describing: caption))]) + return ("pageBlockSlideshow", [("items", items as Any), ("caption", caption as Any)]) case .pageBlockSubheader(let text): - return ("pageBlockSubheader", [("text", String(describing: text))]) + return ("pageBlockSubheader", [("text", text as Any)]) case .pageBlockSubtitle(let text): - return ("pageBlockSubtitle", [("text", String(describing: text))]) + return ("pageBlockSubtitle", [("text", text as Any)]) case .pageBlockTable(let flags, let title, let rows): - return ("pageBlockTable", [("flags", String(describing: flags)), ("title", String(describing: title)), ("rows", String(describing: rows))]) + return ("pageBlockTable", [("flags", flags as Any), ("title", title as Any), ("rows", rows as Any)]) case .pageBlockTitle(let text): - return ("pageBlockTitle", [("text", String(describing: text))]) + return ("pageBlockTitle", [("text", text as Any)]) case .pageBlockUnsupported: return ("pageBlockUnsupported", []) case .pageBlockVideo(let flags, let videoId, let caption): - return ("pageBlockVideo", [("flags", String(describing: flags)), ("videoId", String(describing: videoId)), ("caption", String(describing: caption))]) + return ("pageBlockVideo", [("flags", flags as Any), ("videoId", videoId as Any), ("caption", caption as Any)]) } } @@ -909,7 +909,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageCaption(let text, let credit): - return ("pageCaption", [("text", String(describing: text)), ("credit", String(describing: credit))]) + return ("pageCaption", [("text", text as Any), ("credit", credit as Any)]) } } @@ -963,9 +963,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageListItemBlocks(let blocks): - return ("pageListItemBlocks", [("blocks", String(describing: blocks))]) + return ("pageListItemBlocks", [("blocks", blocks as Any)]) case .pageListItemText(let text): - return ("pageListItemText", [("text", String(describing: text))]) + return ("pageListItemText", [("text", text as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 167777f3267..1ad7545048c 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -29,9 +29,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageListOrderedItemBlocks(let num, let blocks): - return ("pageListOrderedItemBlocks", [("num", String(describing: num)), ("blocks", String(describing: blocks))]) + return ("pageListOrderedItemBlocks", [("num", num as Any), ("blocks", blocks as Any)]) case .pageListOrderedItemText(let num, let text): - return ("pageListOrderedItemText", [("num", String(describing: num)), ("text", String(describing: text))]) + return ("pageListOrderedItemText", [("num", num as Any), ("text", text as Any)]) } } @@ -95,7 +95,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageRelatedArticle(let flags, let url, let webpageId, let title, let description, let photoId, let author, let publishedDate): - return ("pageRelatedArticle", [("flags", String(describing: flags)), ("url", String(describing: url)), ("webpageId", String(describing: webpageId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photoId", String(describing: photoId)), ("author", String(describing: author)), ("publishedDate", String(describing: publishedDate))]) + return ("pageRelatedArticle", [("flags", flags as Any), ("url", url as Any), ("webpageId", webpageId as Any), ("title", title as Any), ("description", description as Any), ("photoId", photoId as Any), ("author", author as Any), ("publishedDate", publishedDate as Any)]) } } @@ -155,7 +155,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageTableCell(let flags, let text, let colspan, let rowspan): - return ("pageTableCell", [("flags", String(describing: flags)), ("text", String(describing: text)), ("colspan", String(describing: colspan)), ("rowspan", String(describing: rowspan))]) + return ("pageTableCell", [("flags", flags as Any), ("text", text as Any), ("colspan", colspan as Any), ("rowspan", rowspan as Any)]) } } @@ -206,7 +206,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pageTableRow(let cells): - return ("pageTableRow", [("cells", String(describing: cells))]) + return ("pageTableRow", [("cells", cells as Any)]) } } @@ -254,7 +254,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow(let salt1, let salt2, let g, let p): - return ("passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow", [("salt1", String(describing: salt1)), ("salt2", String(describing: salt2)), ("g", String(describing: g)), ("p", String(describing: p))]) + return ("passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow", [("salt1", salt1 as Any), ("salt2", salt2 as Any), ("g", g as Any), ("p", p as Any)]) case .passwordKdfAlgoUnknown: return ("passwordKdfAlgoUnknown", []) } @@ -305,7 +305,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentCharge(let id, let providerChargeId): - return ("paymentCharge", [("id", String(describing: id)), ("providerChargeId", String(describing: providerChargeId))]) + return ("paymentCharge", [("id", id as Any), ("providerChargeId", providerChargeId as Any)]) } } @@ -345,7 +345,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentFormMethod(let url, let title): - return ("paymentFormMethod", [("url", String(describing: url)), ("title", String(describing: title))]) + return ("paymentFormMethod", [("url", url as Any), ("title", title as Any)]) } } @@ -388,7 +388,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentRequestedInfo(let flags, let name, let phone, let email, let shippingAddress): - return ("paymentRequestedInfo", [("flags", String(describing: flags)), ("name", String(describing: name)), ("phone", String(describing: phone)), ("email", String(describing: email)), ("shippingAddress", String(describing: shippingAddress))]) + return ("paymentRequestedInfo", [("flags", flags as Any), ("name", name as Any), ("phone", phone as Any), ("email", email as Any), ("shippingAddress", shippingAddress as Any)]) } } @@ -439,7 +439,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentSavedCredentialsCard(let id, let title): - return ("paymentSavedCredentialsCard", [("id", String(describing: id)), ("title", String(describing: title))]) + return ("paymentSavedCredentialsCard", [("id", id as Any), ("title", title as Any)]) } } @@ -492,11 +492,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerChannel(let channelId): - return ("peerChannel", [("channelId", String(describing: channelId))]) + return ("peerChannel", [("channelId", channelId as Any)]) case .peerChat(let chatId): - return ("peerChat", [("chatId", String(describing: chatId))]) + return ("peerChat", [("chatId", chatId as Any)]) case .peerUser(let userId): - return ("peerUser", [("userId", String(describing: userId))]) + return ("peerUser", [("userId", userId as Any)]) } } @@ -555,7 +555,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerBlocked(let peerId, let date): - return ("peerBlocked", [("peerId", String(describing: peerId)), ("date", String(describing: date))]) + return ("peerBlocked", [("peerId", peerId as Any), ("date", date as Any)]) } } @@ -605,9 +605,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerLocated(let peer, let expires, let distance): - return ("peerLocated", [("peer", String(describing: peer)), ("expires", String(describing: expires)), ("distance", String(describing: distance))]) + return ("peerLocated", [("peer", peer as Any), ("expires", expires as Any), ("distance", distance as Any)]) case .peerSelfLocated(let expires): - return ("peerSelfLocated", [("expires", String(describing: expires))]) + return ("peerSelfLocated", [("expires", expires as Any)]) } } @@ -668,7 +668,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let iosSound, let androidSound, let otherSound): - return ("peerNotifySettings", [("flags", String(describing: flags)), ("showPreviews", String(describing: showPreviews)), ("silent", String(describing: silent)), ("muteUntil", String(describing: muteUntil)), ("iosSound", String(describing: iosSound)), ("androidSound", String(describing: androidSound)), ("otherSound", String(describing: otherSound))]) + return ("peerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("iosSound", iosSound as Any), ("androidSound", androidSound as Any), ("otherSound", otherSound as Any)]) } } @@ -735,7 +735,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate): - return ("peerSettings", [("flags", String(describing: flags)), ("geoDistance", String(describing: geoDistance)), ("requestChatTitle", String(describing: requestChatTitle)), ("requestChatDate", String(describing: requestChatDate))]) + return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any)]) } } @@ -853,17 +853,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .phoneCall(let flags, let id, let accessHash, let date, let adminId, let participantId, let gAOrB, let keyFingerprint, let `protocol`, let connections, let startDate): - return ("phoneCall", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("gAOrB", String(describing: gAOrB)), ("keyFingerprint", String(describing: keyFingerprint)), ("`protocol`", String(describing: `protocol`)), ("connections", String(describing: connections)), ("startDate", String(describing: startDate))]) + return ("phoneCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("gAOrB", gAOrB as Any), ("keyFingerprint", keyFingerprint as Any), ("`protocol`", `protocol` as Any), ("connections", connections as Any), ("startDate", startDate as Any)]) case .phoneCallAccepted(let flags, let id, let accessHash, let date, let adminId, let participantId, let gB, let `protocol`): - return ("phoneCallAccepted", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("gB", String(describing: gB)), ("`protocol`", String(describing: `protocol`))]) + return ("phoneCallAccepted", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("gB", gB as Any), ("`protocol`", `protocol` as Any)]) case .phoneCallDiscarded(let flags, let id, let reason, let duration): - return ("phoneCallDiscarded", [("flags", String(describing: flags)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("duration", String(describing: duration))]) + return ("phoneCallDiscarded", [("flags", flags as Any), ("id", id as Any), ("reason", reason as Any), ("duration", duration as Any)]) case .phoneCallEmpty(let id): - return ("phoneCallEmpty", [("id", String(describing: id))]) + return ("phoneCallEmpty", [("id", id as Any)]) case .phoneCallRequested(let flags, let id, let accessHash, let date, let adminId, let participantId, let gAHash, let `protocol`): - return ("phoneCallRequested", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("gAHash", String(describing: gAHash)), ("`protocol`", String(describing: `protocol`))]) + return ("phoneCallRequested", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("gAHash", gAHash as Any), ("`protocol`", `protocol` as Any)]) case .phoneCallWaiting(let flags, let id, let accessHash, let date, let adminId, let participantId, let `protocol`, let receiveDate): - return ("phoneCallWaiting", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("`protocol`", String(describing: `protocol`)), ("receiveDate", String(describing: receiveDate))]) + return ("phoneCallWaiting", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("`protocol`", `protocol` as Any), ("receiveDate", receiveDate as Any)]) } } @@ -1139,7 +1139,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .phoneCallProtocol(let flags, let minLayer, let maxLayer, let libraryVersions): - return ("phoneCallProtocol", [("flags", String(describing: flags)), ("minLayer", String(describing: minLayer)), ("maxLayer", String(describing: maxLayer)), ("libraryVersions", String(describing: libraryVersions))]) + return ("phoneCallProtocol", [("flags", flags as Any), ("minLayer", minLayer as Any), ("maxLayer", maxLayer as Any), ("libraryVersions", libraryVersions as Any)]) } } @@ -1204,9 +1204,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .phoneConnection(let flags, let id, let ip, let ipv6, let port, let peerTag): - return ("phoneConnection", [("flags", String(describing: flags)), ("id", String(describing: id)), ("ip", String(describing: ip)), ("ipv6", String(describing: ipv6)), ("port", String(describing: port)), ("peerTag", String(describing: peerTag))]) + return ("phoneConnection", [("flags", flags as Any), ("id", id as Any), ("ip", ip as Any), ("ipv6", ipv6 as Any), ("port", port as Any), ("peerTag", peerTag as Any)]) case .phoneConnectionWebrtc(let flags, let id, let ip, let ipv6, let port, let username, let password): - return ("phoneConnectionWebrtc", [("flags", String(describing: flags)), ("id", String(describing: id)), ("ip", String(describing: ip)), ("ipv6", String(describing: ipv6)), ("port", String(describing: port)), ("username", String(describing: username)), ("password", String(describing: password))]) + return ("phoneConnectionWebrtc", [("flags", flags as Any), ("id", id as Any), ("ip", ip as Any), ("ipv6", ipv6 as Any), ("port", port as Any), ("username", username as Any), ("password", password as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 81b23ee6e28..7be386d4ccb 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -38,9 +38,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId): - return ("photo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("sizes", String(describing: sizes)), ("videoSizes", String(describing: videoSizes)), ("dcId", String(describing: dcId))]) + return ("photo", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("date", date as Any), ("sizes", sizes as Any), ("videoSizes", videoSizes as Any), ("dcId", dcId as Any)]) case .photoEmpty(let id): - return ("photoEmpty", [("id", String(describing: id))]) + return ("photoEmpty", [("id", id as Any)]) } } @@ -162,17 +162,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .photoCachedSize(let type, let w, let h, let bytes): - return ("photoCachedSize", [("type", String(describing: type)), ("w", String(describing: w)), ("h", String(describing: h)), ("bytes", String(describing: bytes))]) + return ("photoCachedSize", [("type", type as Any), ("w", w as Any), ("h", h as Any), ("bytes", bytes as Any)]) case .photoPathSize(let type, let bytes): - return ("photoPathSize", [("type", String(describing: type)), ("bytes", String(describing: bytes))]) + return ("photoPathSize", [("type", type as Any), ("bytes", bytes as Any)]) case .photoSize(let type, let w, let h, let size): - return ("photoSize", [("type", String(describing: type)), ("w", String(describing: w)), ("h", String(describing: h)), ("size", String(describing: size))]) + return ("photoSize", [("type", type as Any), ("w", w as Any), ("h", h as Any), ("size", size as Any)]) case .photoSizeEmpty(let type): - return ("photoSizeEmpty", [("type", String(describing: type))]) + return ("photoSizeEmpty", [("type", type as Any)]) case .photoSizeProgressive(let type, let w, let h, let sizes): - return ("photoSizeProgressive", [("type", String(describing: type)), ("w", String(describing: w)), ("h", String(describing: h)), ("sizes", String(describing: sizes))]) + return ("photoSizeProgressive", [("type", type as Any), ("w", w as Any), ("h", h as Any), ("sizes", sizes as Any)]) case .photoStrippedSize(let type, let bytes): - return ("photoStrippedSize", [("type", String(describing: type)), ("bytes", String(describing: bytes))]) + return ("photoStrippedSize", [("type", type as Any), ("bytes", bytes as Any)]) } } @@ -307,7 +307,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate): - return ("poll", [("id", String(describing: id)), ("flags", String(describing: flags)), ("question", String(describing: question)), ("answers", String(describing: answers)), ("closePeriod", String(describing: closePeriod)), ("closeDate", String(describing: closeDate))]) + return ("poll", [("id", id as Any), ("flags", flags as Any), ("question", question as Any), ("answers", answers as Any), ("closePeriod", closePeriod as Any), ("closeDate", closeDate as Any)]) } } @@ -361,7 +361,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pollAnswer(let text, let option): - return ("pollAnswer", [("text", String(describing: text)), ("option", String(describing: option))]) + return ("pollAnswer", [("text", text as Any), ("option", option as Any)]) } } @@ -402,7 +402,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pollAnswerVoters(let flags, let option, let voters): - return ("pollAnswerVoters", [("flags", String(describing: flags)), ("option", String(describing: option)), ("voters", String(describing: voters))]) + return ("pollAnswerVoters", [("flags", flags as Any), ("option", option as Any), ("voters", voters as Any)]) } } @@ -461,7 +461,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities): - return ("pollResults", [("flags", String(describing: flags)), ("results", String(describing: results)), ("totalVoters", String(describing: totalVoters)), ("recentVoters", String(describing: recentVoters)), ("solution", String(describing: solution)), ("solutionEntities", String(describing: solutionEntities))]) + return ("pollResults", [("flags", flags as Any), ("results", results as Any), ("totalVoters", totalVoters as Any), ("recentVoters", recentVoters as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) } } @@ -519,7 +519,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .popularContact(let clientId, let importers): - return ("popularContact", [("clientId", String(describing: clientId)), ("importers", String(describing: importers))]) + return ("popularContact", [("clientId", clientId as Any), ("importers", importers as Any)]) } } @@ -563,7 +563,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .postAddress(let streetLine1, let streetLine2, let city, let state, let countryIso2, let postCode): - return ("postAddress", [("streetLine1", String(describing: streetLine1)), ("streetLine2", String(describing: streetLine2)), ("city", String(describing: city)), ("state", String(describing: state)), ("countryIso2", String(describing: countryIso2)), ("postCode", String(describing: postCode))]) + return ("postAddress", [("streetLine1", streetLine1 as Any), ("streetLine2", streetLine2 as Any), ("city", city as Any), ("state", state as Any), ("countryIso2", countryIso2 as Any), ("postCode", postCode as Any)]) } } @@ -619,7 +619,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .premiumGiftOption(let flags, let months, let currency, let amount, let botUrl, let storeProduct): - return ("premiumGiftOption", [("flags", String(describing: flags)), ("months", String(describing: months)), ("currency", String(describing: currency)), ("amount", String(describing: amount)), ("botUrl", String(describing: botUrl)), ("storeProduct", String(describing: storeProduct))]) + return ("premiumGiftOption", [("flags", flags as Any), ("months", months as Any), ("currency", currency as Any), ("amount", amount as Any), ("botUrl", botUrl as Any), ("storeProduct", storeProduct as Any)]) } } @@ -675,7 +675,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .premiumSubscriptionOption(let flags, let months, let currency, let amount, let botUrl, let storeProduct): - return ("premiumSubscriptionOption", [("flags", String(describing: flags)), ("months", String(describing: months)), ("currency", String(describing: currency)), ("amount", String(describing: amount)), ("botUrl", String(describing: botUrl)), ("storeProduct", String(describing: storeProduct))]) + return ("premiumSubscriptionOption", [("flags", flags as Any), ("months", months as Any), ("currency", currency as Any), ("amount", amount as Any), ("botUrl", botUrl as Any), ("storeProduct", storeProduct as Any)]) } } @@ -917,19 +917,19 @@ public extension Api { case .privacyValueAllowAll: return ("privacyValueAllowAll", []) case .privacyValueAllowChatParticipants(let chats): - return ("privacyValueAllowChatParticipants", [("chats", String(describing: chats))]) + return ("privacyValueAllowChatParticipants", [("chats", chats as Any)]) case .privacyValueAllowContacts: return ("privacyValueAllowContacts", []) case .privacyValueAllowUsers(let users): - return ("privacyValueAllowUsers", [("users", String(describing: users))]) + return ("privacyValueAllowUsers", [("users", users as Any)]) case .privacyValueDisallowAll: return ("privacyValueDisallowAll", []) case .privacyValueDisallowChatParticipants(let chats): - return ("privacyValueDisallowChatParticipants", [("chats", String(describing: chats))]) + return ("privacyValueDisallowChatParticipants", [("chats", chats as Any)]) case .privacyValueDisallowContacts: return ("privacyValueDisallowContacts", []) case .privacyValueDisallowUsers(let users): - return ("privacyValueDisallowUsers", [("users", String(describing: users))]) + return ("privacyValueDisallowUsers", [("users", users as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index ce013ce030c..1b151c0e579 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -30,9 +30,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .reactionCustomEmoji(let documentId): - return ("reactionCustomEmoji", [("documentId", String(describing: documentId))]) + return ("reactionCustomEmoji", [("documentId", documentId as Any)]) case .reactionEmoji(let emoticon): - return ("reactionEmoji", [("emoticon", String(describing: emoticon))]) + return ("reactionEmoji", [("emoticon", emoticon as Any)]) case .reactionEmpty: return ("reactionEmpty", []) } @@ -87,7 +87,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .reactionCount(let flags, let chosenOrder, let reaction, let count): - return ("reactionCount", [("flags", String(describing: flags)), ("chosenOrder", String(describing: chosenOrder)), ("reaction", String(describing: reaction)), ("count", String(describing: count))]) + return ("reactionCount", [("flags", flags as Any), ("chosenOrder", chosenOrder as Any), ("reaction", reaction as Any), ("count", count as Any)]) } } @@ -135,7 +135,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .receivedNotifyMessage(let id, let flags): - return ("receivedNotifyMessage", [("id", String(describing: id)), ("flags", String(describing: flags))]) + return ("receivedNotifyMessage", [("id", id as Any), ("flags", flags as Any)]) } } @@ -206,15 +206,15 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .recentMeUrlChat(let url, let chatId): - return ("recentMeUrlChat", [("url", String(describing: url)), ("chatId", String(describing: chatId))]) + return ("recentMeUrlChat", [("url", url as Any), ("chatId", chatId as Any)]) case .recentMeUrlChatInvite(let url, let chatInvite): - return ("recentMeUrlChatInvite", [("url", String(describing: url)), ("chatInvite", String(describing: chatInvite))]) + return ("recentMeUrlChatInvite", [("url", url as Any), ("chatInvite", chatInvite as Any)]) case .recentMeUrlStickerSet(let url, let set): - return ("recentMeUrlStickerSet", [("url", String(describing: url)), ("set", String(describing: set))]) + return ("recentMeUrlStickerSet", [("url", url as Any), ("set", set as Any)]) case .recentMeUrlUnknown(let url): - return ("recentMeUrlUnknown", [("url", String(describing: url))]) + return ("recentMeUrlUnknown", [("url", url as Any)]) case .recentMeUrlUser(let url, let userId): - return ("recentMeUrlUser", [("url", String(describing: url)), ("userId", String(describing: userId))]) + return ("recentMeUrlUser", [("url", url as Any), ("userId", userId as Any)]) } } @@ -342,13 +342,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .replyInlineMarkup(let rows): - return ("replyInlineMarkup", [("rows", String(describing: rows))]) + return ("replyInlineMarkup", [("rows", rows as Any)]) case .replyKeyboardForceReply(let flags, let placeholder): - return ("replyKeyboardForceReply", [("flags", String(describing: flags)), ("placeholder", String(describing: placeholder))]) + return ("replyKeyboardForceReply", [("flags", flags as Any), ("placeholder", placeholder as Any)]) case .replyKeyboardHide(let flags): - return ("replyKeyboardHide", [("flags", String(describing: flags))]) + return ("replyKeyboardHide", [("flags", flags as Any)]) case .replyKeyboardMarkup(let flags, let rows, let placeholder): - return ("replyKeyboardMarkup", [("flags", String(describing: flags)), ("rows", String(describing: rows)), ("placeholder", String(describing: placeholder))]) + return ("replyKeyboardMarkup", [("flags", flags as Any), ("rows", rows as Any), ("placeholder", placeholder as Any)]) } } @@ -568,7 +568,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .restrictionReason(let platform, let reason, let text): - return ("restrictionReason", [("platform", String(describing: platform)), ("reason", String(describing: reason)), ("text", String(describing: text))]) + return ("restrictionReason", [("platform", platform as Any), ("reason", reason as Any), ("text", text as Any)]) } } @@ -726,37 +726,37 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .textAnchor(let text, let name): - return ("textAnchor", [("text", String(describing: text)), ("name", String(describing: name))]) + return ("textAnchor", [("text", text as Any), ("name", name as Any)]) case .textBold(let text): - return ("textBold", [("text", String(describing: text))]) + return ("textBold", [("text", text as Any)]) case .textConcat(let texts): - return ("textConcat", [("texts", String(describing: texts))]) + return ("textConcat", [("texts", texts as Any)]) case .textEmail(let text, let email): - return ("textEmail", [("text", String(describing: text)), ("email", String(describing: email))]) + return ("textEmail", [("text", text as Any), ("email", email as Any)]) case .textEmpty: return ("textEmpty", []) case .textFixed(let text): - return ("textFixed", [("text", String(describing: text))]) + return ("textFixed", [("text", text as Any)]) case .textImage(let documentId, let w, let h): - return ("textImage", [("documentId", String(describing: documentId)), ("w", String(describing: w)), ("h", String(describing: h))]) + return ("textImage", [("documentId", documentId as Any), ("w", w as Any), ("h", h as Any)]) case .textItalic(let text): - return ("textItalic", [("text", String(describing: text))]) + return ("textItalic", [("text", text as Any)]) case .textMarked(let text): - return ("textMarked", [("text", String(describing: text))]) + return ("textMarked", [("text", text as Any)]) case .textPhone(let text, let phone): - return ("textPhone", [("text", String(describing: text)), ("phone", String(describing: phone))]) + return ("textPhone", [("text", text as Any), ("phone", phone as Any)]) case .textPlain(let text): - return ("textPlain", [("text", String(describing: text))]) + return ("textPlain", [("text", text as Any)]) case .textStrike(let text): - return ("textStrike", [("text", String(describing: text))]) + return ("textStrike", [("text", text as Any)]) case .textSubscript(let text): - return ("textSubscript", [("text", String(describing: text))]) + return ("textSubscript", [("text", text as Any)]) case .textSuperscript(let text): - return ("textSuperscript", [("text", String(describing: text))]) + return ("textSuperscript", [("text", text as Any)]) case .textUnderline(let text): - return ("textUnderline", [("text", String(describing: text))]) + return ("textUnderline", [("text", text as Any)]) case .textUrl(let text, let url, let webpageId): - return ("textUrl", [("text", String(describing: text)), ("url", String(describing: url)), ("webpageId", String(describing: webpageId))]) + return ("textUrl", [("text", text as Any), ("url", url as Any), ("webpageId", webpageId as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api18.swift b/submodules/TelegramApi/Sources/Api18.swift index c76b5fa753d..14c977bb4a2 100644 --- a/submodules/TelegramApi/Sources/Api18.swift +++ b/submodules/TelegramApi/Sources/Api18.swift @@ -19,7 +19,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .savedPhoneContact(let phone, let firstName, let lastName, let date): - return ("savedPhoneContact", [("phone", String(describing: phone)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("date", String(describing: date))]) + return ("savedPhoneContact", [("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("date", date as Any)]) } } @@ -67,7 +67,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .searchResultsCalendarPeriod(let date, let minMsgId, let maxMsgId, let count): - return ("searchResultsCalendarPeriod", [("date", String(describing: date)), ("minMsgId", String(describing: minMsgId)), ("maxMsgId", String(describing: maxMsgId)), ("count", String(describing: count))]) + return ("searchResultsCalendarPeriod", [("date", date as Any), ("minMsgId", minMsgId as Any), ("maxMsgId", maxMsgId as Any), ("count", count as Any)]) } } @@ -114,7 +114,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .searchResultPosition(let msgId, let date, let offset): - return ("searchResultPosition", [("msgId", String(describing: msgId)), ("date", String(describing: date)), ("offset", String(describing: offset))]) + return ("searchResultPosition", [("msgId", msgId as Any), ("date", date as Any), ("offset", offset as Any)]) } } @@ -158,7 +158,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureCredentialsEncrypted(let data, let hash, let secret): - return ("secureCredentialsEncrypted", [("data", String(describing: data)), ("hash", String(describing: hash)), ("secret", String(describing: secret))]) + return ("secureCredentialsEncrypted", [("data", data as Any), ("hash", hash as Any), ("secret", secret as Any)]) } } @@ -202,7 +202,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureData(let data, let dataHash, let secret): - return ("secureData", [("data", String(describing: data)), ("dataHash", String(describing: dataHash)), ("secret", String(describing: secret))]) + return ("secureData", [("data", data as Any), ("dataHash", dataHash as Any), ("secret", secret as Any)]) } } @@ -257,7 +257,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureFile(let id, let accessHash, let size, let dcId, let date, let fileHash, let secret): - return ("secureFile", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("size", String(describing: size)), ("dcId", String(describing: dcId)), ("date", String(describing: date)), ("fileHash", String(describing: fileHash)), ("secret", String(describing: secret))]) + return ("secureFile", [("id", id as Any), ("accessHash", accessHash as Any), ("size", size as Any), ("dcId", dcId as Any), ("date", date as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) case .secureFileEmpty: return ("secureFileEmpty", []) } @@ -330,9 +330,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .securePasswordKdfAlgoPBKDF2HMACSHA512iter100000(let salt): - return ("securePasswordKdfAlgoPBKDF2HMACSHA512iter100000", [("salt", String(describing: salt))]) + return ("securePasswordKdfAlgoPBKDF2HMACSHA512iter100000", [("salt", salt as Any)]) case .securePasswordKdfAlgoSHA512(let salt): - return ("securePasswordKdfAlgoSHA512", [("salt", String(describing: salt))]) + return ("securePasswordKdfAlgoSHA512", [("salt", salt as Any)]) case .securePasswordKdfAlgoUnknown: return ("securePasswordKdfAlgoUnknown", []) } @@ -391,9 +391,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .securePlainEmail(let email): - return ("securePlainEmail", [("email", String(describing: email))]) + return ("securePlainEmail", [("email", email as Any)]) case .securePlainPhone(let phone): - return ("securePlainPhone", [("phone", String(describing: phone))]) + return ("securePlainPhone", [("phone", phone as Any)]) } } @@ -452,9 +452,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureRequiredType(let flags, let type): - return ("secureRequiredType", [("flags", String(describing: flags)), ("type", String(describing: type))]) + return ("secureRequiredType", [("flags", flags as Any), ("type", type as Any)]) case .secureRequiredTypeOneOf(let types): - return ("secureRequiredTypeOneOf", [("types", String(describing: types))]) + return ("secureRequiredTypeOneOf", [("types", types as Any)]) } } @@ -510,7 +510,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureSecretSettings(let secureAlgo, let secureSecret, let secureSecretId): - return ("secureSecretSettings", [("secureAlgo", String(describing: secureAlgo)), ("secureSecret", String(describing: secureSecret)), ("secureSecretId", String(describing: secureSecretId))]) + return ("secureSecretSettings", [("secureAlgo", secureAlgo as Any), ("secureSecret", secureSecret as Any), ("secureSecretId", secureSecretId as Any)]) } } @@ -571,7 +571,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData, let hash): - return ("secureValue", [("flags", String(describing: flags)), ("type", String(describing: type)), ("data", String(describing: data)), ("frontSide", String(describing: frontSide)), ("reverseSide", String(describing: reverseSide)), ("selfie", String(describing: selfie)), ("translation", String(describing: translation)), ("files", String(describing: files)), ("plainData", String(describing: plainData)), ("hash", String(describing: hash))]) + return ("secureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any), ("hash", hash as Any)]) } } @@ -733,23 +733,23 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureValueError(let type, let hash, let text): - return ("secureValueError", [("type", String(describing: type)), ("hash", String(describing: hash)), ("text", String(describing: text))]) + return ("secureValueError", [("type", type as Any), ("hash", hash as Any), ("text", text as Any)]) case .secureValueErrorData(let type, let dataHash, let field, let text): - return ("secureValueErrorData", [("type", String(describing: type)), ("dataHash", String(describing: dataHash)), ("field", String(describing: field)), ("text", String(describing: text))]) + return ("secureValueErrorData", [("type", type as Any), ("dataHash", dataHash as Any), ("field", field as Any), ("text", text as Any)]) case .secureValueErrorFile(let type, let fileHash, let text): - return ("secureValueErrorFile", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorFile", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorFiles(let type, let fileHash, let text): - return ("secureValueErrorFiles", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorFiles", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorFrontSide(let type, let fileHash, let text): - return ("secureValueErrorFrontSide", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorFrontSide", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorReverseSide(let type, let fileHash, let text): - return ("secureValueErrorReverseSide", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorReverseSide", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorSelfie(let type, let fileHash, let text): - return ("secureValueErrorSelfie", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorSelfie", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorTranslationFile(let type, let fileHash, let text): - return ("secureValueErrorTranslationFile", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorTranslationFile", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) case .secureValueErrorTranslationFiles(let type, let fileHash, let text): - return ("secureValueErrorTranslationFiles", [("type", String(describing: type)), ("fileHash", String(describing: fileHash)), ("text", String(describing: text))]) + return ("secureValueErrorTranslationFiles", [("type", type as Any), ("fileHash", fileHash as Any), ("text", text as Any)]) } } @@ -953,7 +953,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .secureValueHash(let type, let hash): - return ("secureValueHash", [("type", String(describing: type)), ("hash", String(describing: hash))]) + return ("secureValueHash", [("type", type as Any), ("hash", hash as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift index 9ce012be555..c13f4858800 100644 --- a/submodules/TelegramApi/Sources/Api19.swift +++ b/submodules/TelegramApi/Sources/Api19.swift @@ -17,7 +17,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sendAsPeer(let flags, let peer): - return ("sendAsPeer", [("flags", String(describing: flags)), ("peer", String(describing: peer))]) + return ("sendAsPeer", [("flags", flags as Any), ("peer", peer as Any)]) } } @@ -185,15 +185,15 @@ public extension Api { case .sendMessageChooseStickerAction: return ("sendMessageChooseStickerAction", []) case .sendMessageEmojiInteraction(let emoticon, let msgId, let interaction): - return ("sendMessageEmojiInteraction", [("emoticon", String(describing: emoticon)), ("msgId", String(describing: msgId)), ("interaction", String(describing: interaction))]) + return ("sendMessageEmojiInteraction", [("emoticon", emoticon as Any), ("msgId", msgId as Any), ("interaction", interaction as Any)]) case .sendMessageEmojiInteractionSeen(let emoticon): - return ("sendMessageEmojiInteractionSeen", [("emoticon", String(describing: emoticon))]) + return ("sendMessageEmojiInteractionSeen", [("emoticon", emoticon as Any)]) case .sendMessageGamePlayAction: return ("sendMessageGamePlayAction", []) case .sendMessageGeoLocationAction: return ("sendMessageGeoLocationAction", []) case .sendMessageHistoryImportAction(let progress): - return ("sendMessageHistoryImportAction", [("progress", String(describing: progress))]) + return ("sendMessageHistoryImportAction", [("progress", progress as Any)]) case .sendMessageRecordAudioAction: return ("sendMessageRecordAudioAction", []) case .sendMessageRecordRoundAction: @@ -203,15 +203,15 @@ public extension Api { case .sendMessageTypingAction: return ("sendMessageTypingAction", []) case .sendMessageUploadAudioAction(let progress): - return ("sendMessageUploadAudioAction", [("progress", String(describing: progress))]) + return ("sendMessageUploadAudioAction", [("progress", progress as Any)]) case .sendMessageUploadDocumentAction(let progress): - return ("sendMessageUploadDocumentAction", [("progress", String(describing: progress))]) + return ("sendMessageUploadDocumentAction", [("progress", progress as Any)]) case .sendMessageUploadPhotoAction(let progress): - return ("sendMessageUploadPhotoAction", [("progress", String(describing: progress))]) + return ("sendMessageUploadPhotoAction", [("progress", progress as Any)]) case .sendMessageUploadRoundAction(let progress): - return ("sendMessageUploadRoundAction", [("progress", String(describing: progress))]) + return ("sendMessageUploadRoundAction", [("progress", progress as Any)]) case .sendMessageUploadVideoAction(let progress): - return ("sendMessageUploadVideoAction", [("progress", String(describing: progress))]) + return ("sendMessageUploadVideoAction", [("progress", progress as Any)]) case .speakingInGroupCallAction: return ("speakingInGroupCallAction", []) } @@ -370,7 +370,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .shippingOption(let id, let title, let prices): - return ("shippingOption", [("id", String(describing: id)), ("title", String(describing: title)), ("prices", String(describing: prices))]) + return ("shippingOption", [("id", id as Any), ("title", title as Any), ("prices", prices as Any)]) } } @@ -414,7 +414,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .simpleWebViewResultUrl(let url): - return ("simpleWebViewResultUrl", [("url", String(describing: url))]) + return ("simpleWebViewResultUrl", [("url", url as Any)]) } } @@ -462,7 +462,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sponsoredMessage(let flags, let randomId, let fromId, let chatInvite, let chatInviteHash, let channelPost, let startParam, let message, let entities): - return ("sponsoredMessage", [("flags", String(describing: flags)), ("randomId", String(describing: randomId)), ("fromId", String(describing: fromId)), ("chatInvite", String(describing: chatInvite)), ("chatInviteHash", String(describing: chatInviteHash)), ("channelPost", String(describing: channelPost)), ("startParam", String(describing: startParam)), ("message", String(describing: message)), ("entities", String(describing: entities))]) + return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("fromId", fromId as Any), ("chatInvite", chatInvite as Any), ("chatInviteHash", chatInviteHash as Any), ("channelPost", channelPost as Any), ("startParam", startParam as Any), ("message", message as Any), ("entities", entities as Any)]) } } @@ -529,7 +529,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsAbsValueAndPrev(let current, let previous): - return ("statsAbsValueAndPrev", [("current", String(describing: current)), ("previous", String(describing: previous))]) + return ("statsAbsValueAndPrev", [("current", current as Any), ("previous", previous as Any)]) } } @@ -569,7 +569,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsDateRangeDays(let minDate, let maxDate): - return ("statsDateRangeDays", [("minDate", String(describing: minDate)), ("maxDate", String(describing: maxDate))]) + return ("statsDateRangeDays", [("minDate", minDate as Any), ("maxDate", maxDate as Any)]) } } @@ -624,11 +624,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsGraph(let flags, let json, let zoomToken): - return ("statsGraph", [("flags", String(describing: flags)), ("json", String(describing: json)), ("zoomToken", String(describing: zoomToken))]) + return ("statsGraph", [("flags", flags as Any), ("json", json as Any), ("zoomToken", zoomToken as Any)]) case .statsGraphAsync(let token): - return ("statsGraphAsync", [("token", String(describing: token))]) + return ("statsGraphAsync", [("token", token as Any)]) case .statsGraphError(let error): - return ("statsGraphError", [("error", String(describing: error))]) + return ("statsGraphError", [("error", error as Any)]) } } @@ -697,7 +697,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsGroupTopAdmin(let userId, let deleted, let kicked, let banned): - return ("statsGroupTopAdmin", [("userId", String(describing: userId)), ("deleted", String(describing: deleted)), ("kicked", String(describing: kicked)), ("banned", String(describing: banned))]) + return ("statsGroupTopAdmin", [("userId", userId as Any), ("deleted", deleted as Any), ("kicked", kicked as Any), ("banned", banned as Any)]) } } @@ -743,7 +743,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsGroupTopInviter(let userId, let invitations): - return ("statsGroupTopInviter", [("userId", String(describing: userId)), ("invitations", String(describing: invitations))]) + return ("statsGroupTopInviter", [("userId", userId as Any), ("invitations", invitations as Any)]) } } @@ -784,7 +784,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsGroupTopPoster(let userId, let messages, let avgChars): - return ("statsGroupTopPoster", [("userId", String(describing: userId)), ("messages", String(describing: messages)), ("avgChars", String(describing: avgChars))]) + return ("statsGroupTopPoster", [("userId", userId as Any), ("messages", messages as Any), ("avgChars", avgChars as Any)]) } } @@ -827,7 +827,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsPercentValue(let part, let total): - return ("statsPercentValue", [("part", String(describing: part)), ("total", String(describing: total))]) + return ("statsPercentValue", [("part", part as Any), ("total", total as Any)]) } } @@ -866,7 +866,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .statsURL(let url): - return ("statsURL", [("url", String(describing: url))]) + return ("statsURL", [("url", url as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index e44293cb8a3..c6ec2e75788 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -88,17 +88,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): - return ("botInlineMessageMediaAuto", [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageMediaAuto", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): - return ("botInlineMessageMediaContact", [("flags", String(describing: flags)), ("phoneNumber", String(describing: phoneNumber)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("vcard", String(describing: vcard)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageMediaContact", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaGeo(let flags, let geo, let heading, let period, let proximityNotificationRadius, let replyMarkup): - return ("botInlineMessageMediaGeo", [("flags", String(describing: flags)), ("geo", String(describing: geo)), ("heading", String(describing: heading)), ("period", String(describing: period)), ("proximityNotificationRadius", String(describing: proximityNotificationRadius)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageMediaGeo", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaInvoice(let flags, let title, let description, let photo, let currency, let totalAmount, let replyMarkup): - return ("botInlineMessageMediaInvoice", [("flags", String(describing: flags)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageMediaVenue(let flags, let geo, let title, let address, let provider, let venueId, let venueType, let replyMarkup): - return ("botInlineMessageMediaVenue", [("flags", String(describing: flags)), ("geo", String(describing: geo)), ("title", String(describing: title)), ("address", String(describing: address)), ("provider", String(describing: provider)), ("venueId", String(describing: venueId)), ("venueType", String(describing: venueType)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageMediaVenue", [("flags", flags as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) case .botInlineMessageText(let flags, let message, let entities, let replyMarkup): - return ("botInlineMessageText", [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("replyMarkup", String(describing: replyMarkup))]) + return ("botInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } } @@ -320,9 +320,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botInlineMediaResult(let flags, let id, let type, let photo, let document, let title, let description, let sendMessage): - return ("botInlineMediaResult", [("flags", String(describing: flags)), ("id", String(describing: id)), ("type", String(describing: type)), ("photo", String(describing: photo)), ("document", String(describing: document)), ("title", String(describing: title)), ("description", String(describing: description)), ("sendMessage", String(describing: sendMessage))]) + return ("botInlineMediaResult", [("flags", flags as Any), ("id", id as Any), ("type", type as Any), ("photo", photo as Any), ("document", document as Any), ("title", title as Any), ("description", description as Any), ("sendMessage", sendMessage as Any)]) case .botInlineResult(let flags, let id, let type, let title, let description, let url, let thumb, let content, let sendMessage): - return ("botInlineResult", [("flags", String(describing: flags)), ("id", String(describing: id)), ("type", String(describing: type)), ("title", String(describing: title)), ("description", String(describing: description)), ("url", String(describing: url)), ("thumb", String(describing: thumb)), ("content", String(describing: content)), ("sendMessage", String(describing: sendMessage))]) + return ("botInlineResult", [("flags", flags as Any), ("id", id as Any), ("type", type as Any), ("title", title as Any), ("description", description as Any), ("url", url as Any), ("thumb", thumb as Any), ("content", content as Any), ("sendMessage", sendMessage as Any)]) } } @@ -441,7 +441,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botMenuButton(let text, let url): - return ("botMenuButton", [("text", String(describing: text)), ("url", String(describing: url))]) + return ("botMenuButton", [("text", text as Any), ("url", url as Any)]) case .botMenuButtonCommands: return ("botMenuButtonCommands", []) case .botMenuButtonDefault: @@ -494,7 +494,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .cdnConfig(let publicKeys): - return ("cdnConfig", [("publicKeys", String(describing: publicKeys))]) + return ("cdnConfig", [("publicKeys", publicKeys as Any)]) } } @@ -533,7 +533,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .cdnPublicKey(let dcId, let publicKey): - return ("cdnPublicKey", [("dcId", String(describing: dcId)), ("publicKey", String(describing: publicKey))]) + return ("cdnPublicKey", [("dcId", dcId as Any), ("publicKey", publicKey as Any)]) } } @@ -575,7 +575,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelAdminLogEvent(let id, let date, let userId, let action): - return ("channelAdminLogEvent", [("id", String(describing: id)), ("date", String(describing: date)), ("userId", String(describing: userId)), ("action", String(describing: action))]) + return ("channelAdminLogEvent", [("id", id as Any), ("date", date as Any), ("userId", userId as Any), ("action", action as Any)]) } } @@ -640,6 +640,7 @@ public extension Api { case channelAdminLogEventActionSendMessage(message: Api.Message) case channelAdminLogEventActionStartGroupCall(call: Api.InputGroupCall) case channelAdminLogEventActionStopPoll(message: Api.Message) + case channelAdminLogEventActionToggleAntiSpam(newValue: Api.Bool) case channelAdminLogEventActionToggleForum(newValue: Api.Bool) case channelAdminLogEventActionToggleGroupCallSetting(joinMuted: Api.Bool) case channelAdminLogEventActionToggleInvites(newValue: Api.Bool) @@ -882,6 +883,12 @@ public extension Api { } message.serialize(buffer, true) break + case .channelAdminLogEventActionToggleAntiSpam(let newValue): + if boxed { + buffer.appendInt32(1693675004) + } + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionToggleForum(let newValue): if boxed { buffer.appendInt32(46949251) @@ -937,89 +944,91 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelAdminLogEventActionChangeAbout(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeAbout", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeAbout", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeAvailableReactions(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeAvailableReactions", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeAvailableReactions", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeHistoryTTL", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeHistoryTTL", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeLinkedChat(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeLinkedChat", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeLinkedChat", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeLocation(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeLocation", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeLocation", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangePhoto(let prevPhoto, let newPhoto): - return ("channelAdminLogEventActionChangePhoto", [("prevPhoto", String(describing: prevPhoto)), ("newPhoto", String(describing: newPhoto))]) + return ("channelAdminLogEventActionChangePhoto", [("prevPhoto", prevPhoto as Any), ("newPhoto", newPhoto as Any)]) case .channelAdminLogEventActionChangeStickerSet(let prevStickerset, let newStickerset): - return ("channelAdminLogEventActionChangeStickerSet", [("prevStickerset", String(describing: prevStickerset)), ("newStickerset", String(describing: newStickerset))]) + return ("channelAdminLogEventActionChangeStickerSet", [("prevStickerset", prevStickerset as Any), ("newStickerset", newStickerset as Any)]) case .channelAdminLogEventActionChangeTitle(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeTitle", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeTitle", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeUsername(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeUsername", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeUsername", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeUsernames(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeUsernames", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionChangeUsernames", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionCreateTopic(let topic): - return ("channelAdminLogEventActionCreateTopic", [("topic", String(describing: topic))]) + return ("channelAdminLogEventActionCreateTopic", [("topic", topic as Any)]) case .channelAdminLogEventActionDefaultBannedRights(let prevBannedRights, let newBannedRights): - return ("channelAdminLogEventActionDefaultBannedRights", [("prevBannedRights", String(describing: prevBannedRights)), ("newBannedRights", String(describing: newBannedRights))]) + return ("channelAdminLogEventActionDefaultBannedRights", [("prevBannedRights", prevBannedRights as Any), ("newBannedRights", newBannedRights as Any)]) case .channelAdminLogEventActionDeleteMessage(let message): - return ("channelAdminLogEventActionDeleteMessage", [("message", String(describing: message))]) + return ("channelAdminLogEventActionDeleteMessage", [("message", message as Any)]) case .channelAdminLogEventActionDeleteTopic(let topic): - return ("channelAdminLogEventActionDeleteTopic", [("topic", String(describing: topic))]) + return ("channelAdminLogEventActionDeleteTopic", [("topic", topic as Any)]) case .channelAdminLogEventActionDiscardGroupCall(let call): - return ("channelAdminLogEventActionDiscardGroupCall", [("call", String(describing: call))]) + return ("channelAdminLogEventActionDiscardGroupCall", [("call", call as Any)]) case .channelAdminLogEventActionEditMessage(let prevMessage, let newMessage): - return ("channelAdminLogEventActionEditMessage", [("prevMessage", String(describing: prevMessage)), ("newMessage", String(describing: newMessage))]) + return ("channelAdminLogEventActionEditMessage", [("prevMessage", prevMessage as Any), ("newMessage", newMessage as Any)]) case .channelAdminLogEventActionEditTopic(let prevTopic, let newTopic): - return ("channelAdminLogEventActionEditTopic", [("prevTopic", String(describing: prevTopic)), ("newTopic", String(describing: newTopic))]) + return ("channelAdminLogEventActionEditTopic", [("prevTopic", prevTopic as Any), ("newTopic", newTopic as Any)]) case .channelAdminLogEventActionExportedInviteDelete(let invite): - return ("channelAdminLogEventActionExportedInviteDelete", [("invite", String(describing: invite))]) + return ("channelAdminLogEventActionExportedInviteDelete", [("invite", invite as Any)]) case .channelAdminLogEventActionExportedInviteEdit(let prevInvite, let newInvite): - return ("channelAdminLogEventActionExportedInviteEdit", [("prevInvite", String(describing: prevInvite)), ("newInvite", String(describing: newInvite))]) + return ("channelAdminLogEventActionExportedInviteEdit", [("prevInvite", prevInvite as Any), ("newInvite", newInvite as Any)]) case .channelAdminLogEventActionExportedInviteRevoke(let invite): - return ("channelAdminLogEventActionExportedInviteRevoke", [("invite", String(describing: invite))]) + return ("channelAdminLogEventActionExportedInviteRevoke", [("invite", invite as Any)]) case .channelAdminLogEventActionParticipantInvite(let participant): - return ("channelAdminLogEventActionParticipantInvite", [("participant", String(describing: participant))]) + return ("channelAdminLogEventActionParticipantInvite", [("participant", participant as Any)]) case .channelAdminLogEventActionParticipantJoin: return ("channelAdminLogEventActionParticipantJoin", []) case .channelAdminLogEventActionParticipantJoinByInvite(let invite): - return ("channelAdminLogEventActionParticipantJoinByInvite", [("invite", String(describing: invite))]) + return ("channelAdminLogEventActionParticipantJoinByInvite", [("invite", invite as Any)]) case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy): - return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", String(describing: invite)), ("approvedBy", String(describing: approvedBy))]) + return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite as Any), ("approvedBy", approvedBy as Any)]) case .channelAdminLogEventActionParticipantLeave: return ("channelAdminLogEventActionParticipantLeave", []) case .channelAdminLogEventActionParticipantMute(let participant): - return ("channelAdminLogEventActionParticipantMute", [("participant", String(describing: participant))]) + return ("channelAdminLogEventActionParticipantMute", [("participant", participant as Any)]) case .channelAdminLogEventActionParticipantToggleAdmin(let prevParticipant, let newParticipant): - return ("channelAdminLogEventActionParticipantToggleAdmin", [("prevParticipant", String(describing: prevParticipant)), ("newParticipant", String(describing: newParticipant))]) + return ("channelAdminLogEventActionParticipantToggleAdmin", [("prevParticipant", prevParticipant as Any), ("newParticipant", newParticipant as Any)]) case .channelAdminLogEventActionParticipantToggleBan(let prevParticipant, let newParticipant): - return ("channelAdminLogEventActionParticipantToggleBan", [("prevParticipant", String(describing: prevParticipant)), ("newParticipant", String(describing: newParticipant))]) + return ("channelAdminLogEventActionParticipantToggleBan", [("prevParticipant", prevParticipant as Any), ("newParticipant", newParticipant as Any)]) case .channelAdminLogEventActionParticipantUnmute(let participant): - return ("channelAdminLogEventActionParticipantUnmute", [("participant", String(describing: participant))]) + return ("channelAdminLogEventActionParticipantUnmute", [("participant", participant as Any)]) case .channelAdminLogEventActionParticipantVolume(let participant): - return ("channelAdminLogEventActionParticipantVolume", [("participant", String(describing: participant))]) + return ("channelAdminLogEventActionParticipantVolume", [("participant", participant as Any)]) case .channelAdminLogEventActionPinTopic(let flags, let prevTopic, let newTopic): - return ("channelAdminLogEventActionPinTopic", [("flags", String(describing: flags)), ("prevTopic", String(describing: prevTopic)), ("newTopic", String(describing: newTopic))]) + return ("channelAdminLogEventActionPinTopic", [("flags", flags as Any), ("prevTopic", prevTopic as Any), ("newTopic", newTopic as Any)]) case .channelAdminLogEventActionSendMessage(let message): - return ("channelAdminLogEventActionSendMessage", [("message", String(describing: message))]) + return ("channelAdminLogEventActionSendMessage", [("message", message as Any)]) case .channelAdminLogEventActionStartGroupCall(let call): - return ("channelAdminLogEventActionStartGroupCall", [("call", String(describing: call))]) + return ("channelAdminLogEventActionStartGroupCall", [("call", call as Any)]) case .channelAdminLogEventActionStopPoll(let message): - return ("channelAdminLogEventActionStopPoll", [("message", String(describing: message))]) + return ("channelAdminLogEventActionStopPoll", [("message", message as Any)]) + case .channelAdminLogEventActionToggleAntiSpam(let newValue): + return ("channelAdminLogEventActionToggleAntiSpam", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleForum(let newValue): - return ("channelAdminLogEventActionToggleForum", [("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionToggleForum", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleGroupCallSetting(let joinMuted): - return ("channelAdminLogEventActionToggleGroupCallSetting", [("joinMuted", String(describing: joinMuted))]) + return ("channelAdminLogEventActionToggleGroupCallSetting", [("joinMuted", joinMuted as Any)]) case .channelAdminLogEventActionToggleInvites(let newValue): - return ("channelAdminLogEventActionToggleInvites", [("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionToggleInvites", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleNoForwards(let newValue): - return ("channelAdminLogEventActionToggleNoForwards", [("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionToggleNoForwards", [("newValue", newValue as Any)]) case .channelAdminLogEventActionTogglePreHistoryHidden(let newValue): - return ("channelAdminLogEventActionTogglePreHistoryHidden", [("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionTogglePreHistoryHidden", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleSignatures(let newValue): - return ("channelAdminLogEventActionToggleSignatures", [("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionToggleSignatures", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleSlowMode(let prevValue, let newValue): - return ("channelAdminLogEventActionToggleSlowMode", [("prevValue", String(describing: prevValue)), ("newValue", String(describing: newValue))]) + return ("channelAdminLogEventActionToggleSlowMode", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionUpdatePinned(let message): - return ("channelAdminLogEventActionUpdatePinned", [("message", String(describing: message))]) + return ("channelAdminLogEventActionUpdatePinned", [("message", message as Any)]) } } @@ -1516,6 +1525,19 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionToggleAntiSpam(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.Bool? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Bool + } + let _c1 = _1 != nil + if _c1 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionToggleAntiSpam(newValue: _1!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionToggleForum(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 9d016b2a929..cc61c83526b 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -21,7 +21,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerKeyword(let documentId, let keyword): - return ("stickerKeyword", [("documentId", String(describing: documentId)), ("keyword", String(describing: keyword))]) + return ("stickerKeyword", [("documentId", documentId as Any), ("keyword", keyword as Any)]) } } @@ -67,7 +67,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerPack(let emoticon, let documents): - return ("stickerPack", [("emoticon", String(describing: emoticon)), ("documents", String(describing: documents))]) + return ("stickerPack", [("emoticon", emoticon as Any), ("documents", documents as Any)]) } } @@ -123,7 +123,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerSet(let flags, let installedDate, let id, let accessHash, let title, let shortName, let thumbs, let thumbDcId, let thumbVersion, let thumbDocumentId, let count, let hash): - return ("stickerSet", [("flags", String(describing: flags)), ("installedDate", String(describing: installedDate)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("title", String(describing: title)), ("shortName", String(describing: shortName)), ("thumbs", String(describing: thumbs)), ("thumbDcId", String(describing: thumbDcId)), ("thumbVersion", String(describing: thumbVersion)), ("thumbDocumentId", String(describing: thumbDocumentId)), ("count", String(describing: count)), ("hash", String(describing: hash))]) + return ("stickerSet", [("flags", flags as Any), ("installedDate", installedDate as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("shortName", shortName as Any), ("thumbs", thumbs as Any), ("thumbDcId", thumbDcId as Any), ("thumbVersion", thumbVersion as Any), ("thumbDocumentId", thumbDocumentId as Any), ("count", count as Any), ("hash", hash as Any)]) } } @@ -181,6 +181,7 @@ public extension Api { case stickerSetCovered(set: Api.StickerSet, cover: Api.Document) case stickerSetFullCovered(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) case stickerSetMultiCovered(set: Api.StickerSet, covers: [Api.Document]) + case stickerSetNoCovered(set: Api.StickerSet) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -223,17 +224,25 @@ public extension Api { item.serialize(buffer, true) } break + case .stickerSetNoCovered(let set): + if boxed { + buffer.appendInt32(2008112412) + } + set.serialize(buffer, true) + break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerSetCovered(let set, let cover): - return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))]) + return ("stickerSetCovered", [("set", set as Any), ("cover", cover as Any)]) case .stickerSetFullCovered(let set, let packs, let keywords, let documents): - return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("keywords", String(describing: keywords)), ("documents", String(describing: documents))]) + return ("stickerSetFullCovered", [("set", set as Any), ("packs", packs as Any), ("keywords", keywords as Any), ("documents", documents as Any)]) case .stickerSetMultiCovered(let set, let covers): - return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))]) + return ("stickerSetMultiCovered", [("set", set as Any), ("covers", covers as Any)]) + case .stickerSetNoCovered(let set): + return ("stickerSetNoCovered", [("set", set as Any)]) } } @@ -301,6 +310,19 @@ public extension Api { return nil } } + public static func parse_stickerSetNoCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + let _c1 = _1 != nil + if _c1 { + return Api.StickerSetCovered.stickerSetNoCovered(set: _1!) + } + else { + return nil + } + } } } @@ -334,7 +356,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): - return ("theme", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("slug", String(describing: slug)), ("title", String(describing: title)), ("document", String(describing: document)), ("settings", String(describing: settings)), ("emoticon", String(describing: emoticon)), ("installsCount", String(describing: installsCount))]) + return ("theme", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("title", title as Any), ("document", document as Any), ("settings", settings as Any), ("emoticon", emoticon as Any), ("installsCount", installsCount as Any)]) } } @@ -407,7 +429,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): - return ("themeSettings", [("flags", String(describing: flags)), ("baseTheme", String(describing: baseTheme)), ("accentColor", String(describing: accentColor)), ("outboxAccentColor", String(describing: outboxAccentColor)), ("messageColors", String(describing: messageColors)), ("wallpaper", String(describing: wallpaper))]) + return ("themeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any)]) } } @@ -465,7 +487,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .topPeer(let peer, let rating): - return ("topPeer", [("peer", String(describing: peer)), ("rating", String(describing: rating))]) + return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) } } @@ -624,7 +646,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .topPeerCategoryPeers(let category, let count, let peers): - return ("topPeerCategoryPeers", [("category", String(describing: category)), ("count", String(describing: count)), ("peers", String(describing: peers))]) + return ("topPeerCategoryPeers", [("category", category as Any), ("count", count as Any), ("peers", peers as Any)]) } } @@ -755,10 +777,10 @@ public extension Api { case updateStickerSetsOrder(flags: Int32, order: [Int64]) case updateTheme(theme: Api.Theme) case updateTranscribedAudio(flags: Int32, peer: Api.Peer, msgId: Int32, transcriptionId: Int64, text: String) + case updateUser(userId: Int64) case updateUserEmojiStatus(userId: Int64, emojiStatus: Api.EmojiStatus) case updateUserName(userId: Int64, firstName: String, lastName: String, usernames: [Api.Username]) case updateUserPhone(userId: Int64, phone: String) - case updateUserPhoto(userId: Int64, date: Int32, photo: Api.UserProfilePhoto, previous: Api.Bool) case updateUserStatus(userId: Int64, status: Api.UserStatus) case updateUserTyping(userId: Int64, action: Api.SendMessageAction) case updateWebPage(webpage: Api.WebPage, pts: Int32, ptsCount: Int32) @@ -1649,6 +1671,12 @@ public extension Api { serializeInt64(transcriptionId, buffer: buffer, boxed: false) serializeString(text, buffer: buffer, boxed: false) break + case .updateUser(let userId): + if boxed { + buffer.appendInt32(542282808) + } + serializeInt64(userId, buffer: buffer, boxed: false) + break case .updateUserEmojiStatus(let userId, let emojiStatus): if boxed { buffer.appendInt32(674706841) @@ -1676,15 +1704,6 @@ public extension Api { serializeInt64(userId, buffer: buffer, boxed: false) serializeString(phone, buffer: buffer, boxed: false) break - case .updateUserPhoto(let userId, let date, let photo, let previous): - if boxed { - buffer.appendInt32(-232290676) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - photo.serialize(buffer, true) - previous.serialize(buffer, true) - break case .updateUserStatus(let userId, let status): if boxed { buffer.appendInt32(-440534818) @@ -1721,185 +1740,185 @@ public extension Api { case .updateAttachMenuBots: return ("updateAttachMenuBots", []) case .updateBotCallbackQuery(let flags, let queryId, let userId, let peer, let msgId, let chatInstance, let data, let gameShortName): - return ("updateBotCallbackQuery", [("flags", String(describing: flags)), ("queryId", String(describing: queryId)), ("userId", String(describing: userId)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("chatInstance", String(describing: chatInstance)), ("data", String(describing: data)), ("gameShortName", String(describing: gameShortName))]) + return ("updateBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("chatInstance", chatInstance as Any), ("data", data as Any), ("gameShortName", gameShortName as Any)]) case .updateBotChatInviteRequester(let peer, let date, let userId, let about, let invite, let qts): - return ("updateBotChatInviteRequester", [("peer", String(describing: peer)), ("date", String(describing: date)), ("userId", String(describing: userId)), ("about", String(describing: about)), ("invite", String(describing: invite)), ("qts", String(describing: qts))]) + return ("updateBotChatInviteRequester", [("peer", peer as Any), ("date", date as Any), ("userId", userId as Any), ("about", about as Any), ("invite", invite as Any), ("qts", qts as Any)]) case .updateBotCommands(let peer, let botId, let commands): - return ("updateBotCommands", [("peer", String(describing: peer)), ("botId", String(describing: botId)), ("commands", String(describing: commands))]) + return ("updateBotCommands", [("peer", peer as Any), ("botId", botId as Any), ("commands", commands as Any)]) case .updateBotInlineQuery(let flags, let queryId, let userId, let query, let geo, let peerType, let offset): - return ("updateBotInlineQuery", [("flags", String(describing: flags)), ("queryId", String(describing: queryId)), ("userId", String(describing: userId)), ("query", String(describing: query)), ("geo", String(describing: geo)), ("peerType", String(describing: peerType)), ("offset", String(describing: offset))]) + return ("updateBotInlineQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("query", query as Any), ("geo", geo as Any), ("peerType", peerType as Any), ("offset", offset as Any)]) case .updateBotInlineSend(let flags, let userId, let query, let geo, let id, let msgId): - return ("updateBotInlineSend", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("query", String(describing: query)), ("geo", String(describing: geo)), ("id", String(describing: id)), ("msgId", String(describing: msgId))]) + return ("updateBotInlineSend", [("flags", flags as Any), ("userId", userId as Any), ("query", query as Any), ("geo", geo as Any), ("id", id as Any), ("msgId", msgId as Any)]) case .updateBotMenuButton(let botId, let button): - return ("updateBotMenuButton", [("botId", String(describing: botId)), ("button", String(describing: button))]) + return ("updateBotMenuButton", [("botId", botId as Any), ("button", button as Any)]) case .updateBotPrecheckoutQuery(let flags, let queryId, let userId, let payload, let info, let shippingOptionId, let currency, let totalAmount): - return ("updateBotPrecheckoutQuery", [("flags", String(describing: flags)), ("queryId", String(describing: queryId)), ("userId", String(describing: userId)), ("payload", String(describing: payload)), ("info", String(describing: info)), ("shippingOptionId", String(describing: shippingOptionId)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount))]) + return ("updateBotPrecheckoutQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("info", info as Any), ("shippingOptionId", shippingOptionId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any)]) case .updateBotShippingQuery(let queryId, let userId, let payload, let shippingAddress): - return ("updateBotShippingQuery", [("queryId", String(describing: queryId)), ("userId", String(describing: userId)), ("payload", String(describing: payload)), ("shippingAddress", String(describing: shippingAddress))]) + return ("updateBotShippingQuery", [("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("shippingAddress", shippingAddress as Any)]) case .updateBotStopped(let userId, let date, let stopped, let qts): - return ("updateBotStopped", [("userId", String(describing: userId)), ("date", String(describing: date)), ("stopped", String(describing: stopped)), ("qts", String(describing: qts))]) + return ("updateBotStopped", [("userId", userId as Any), ("date", date as Any), ("stopped", stopped as Any), ("qts", qts as Any)]) case .updateBotWebhookJSON(let data): - return ("updateBotWebhookJSON", [("data", String(describing: data))]) + return ("updateBotWebhookJSON", [("data", data as Any)]) case .updateBotWebhookJSONQuery(let queryId, let data, let timeout): - return ("updateBotWebhookJSONQuery", [("queryId", String(describing: queryId)), ("data", String(describing: data)), ("timeout", String(describing: timeout))]) + return ("updateBotWebhookJSONQuery", [("queryId", queryId as Any), ("data", data as Any), ("timeout", timeout as Any)]) case .updateChannel(let channelId): - return ("updateChannel", [("channelId", String(describing: channelId))]) + return ("updateChannel", [("channelId", channelId as Any)]) case .updateChannelAvailableMessages(let channelId, let availableMinId): - return ("updateChannelAvailableMessages", [("channelId", String(describing: channelId)), ("availableMinId", String(describing: availableMinId))]) + return ("updateChannelAvailableMessages", [("channelId", channelId as Any), ("availableMinId", availableMinId as Any)]) case .updateChannelMessageForwards(let channelId, let id, let forwards): - return ("updateChannelMessageForwards", [("channelId", String(describing: channelId)), ("id", String(describing: id)), ("forwards", String(describing: forwards))]) + return ("updateChannelMessageForwards", [("channelId", channelId as Any), ("id", id as Any), ("forwards", forwards as Any)]) case .updateChannelMessageViews(let channelId, let id, let views): - return ("updateChannelMessageViews", [("channelId", String(describing: channelId)), ("id", String(describing: id)), ("views", String(describing: views))]) + return ("updateChannelMessageViews", [("channelId", channelId as Any), ("id", id as Any), ("views", views as Any)]) case .updateChannelParticipant(let flags, let channelId, let date, let actorId, let userId, let prevParticipant, let newParticipant, let invite, let qts): - return ("updateChannelParticipant", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("date", String(describing: date)), ("actorId", String(describing: actorId)), ("userId", String(describing: userId)), ("prevParticipant", String(describing: prevParticipant)), ("newParticipant", String(describing: newParticipant)), ("invite", String(describing: invite)), ("qts", String(describing: qts))]) + return ("updateChannelParticipant", [("flags", flags as Any), ("channelId", channelId as Any), ("date", date as Any), ("actorId", actorId as Any), ("userId", userId as Any), ("prevParticipant", prevParticipant as Any), ("newParticipant", newParticipant as Any), ("invite", invite as Any), ("qts", qts as Any)]) case .updateChannelPinnedTopic(let flags, let channelId, let topicId): - return ("updateChannelPinnedTopic", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topicId", String(describing: topicId))]) + return ("updateChannelPinnedTopic", [("flags", flags as Any), ("channelId", channelId as Any), ("topicId", topicId as Any)]) case .updateChannelPinnedTopics(let flags, let channelId, let order): - return ("updateChannelPinnedTopics", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("order", String(describing: order))]) + return ("updateChannelPinnedTopics", [("flags", flags as Any), ("channelId", channelId as Any), ("order", order as Any)]) case .updateChannelReadMessagesContents(let flags, let channelId, let topMsgId, let messages): - return ("updateChannelReadMessagesContents", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topMsgId", String(describing: topMsgId)), ("messages", String(describing: messages))]) + return ("updateChannelReadMessagesContents", [("flags", flags as Any), ("channelId", channelId as Any), ("topMsgId", topMsgId as Any), ("messages", messages as Any)]) case .updateChannelTooLong(let flags, let channelId, let pts): - return ("updateChannelTooLong", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("pts", String(describing: pts))]) + return ("updateChannelTooLong", [("flags", flags as Any), ("channelId", channelId as Any), ("pts", pts as Any)]) case .updateChannelUserTyping(let flags, let channelId, let topMsgId, let fromId, let action): - return ("updateChannelUserTyping", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topMsgId", String(describing: topMsgId)), ("fromId", String(describing: fromId)), ("action", String(describing: action))]) + return ("updateChannelUserTyping", [("flags", flags as Any), ("channelId", channelId as Any), ("topMsgId", topMsgId as Any), ("fromId", fromId as Any), ("action", action as Any)]) case .updateChannelWebPage(let channelId, let webpage, let pts, let ptsCount): - return ("updateChannelWebPage", [("channelId", String(describing: channelId)), ("webpage", String(describing: webpage)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateChannelWebPage", [("channelId", channelId as Any), ("webpage", webpage as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateChat(let chatId): - return ("updateChat", [("chatId", String(describing: chatId))]) + return ("updateChat", [("chatId", chatId as Any)]) case .updateChatDefaultBannedRights(let peer, let defaultBannedRights, let version): - return ("updateChatDefaultBannedRights", [("peer", String(describing: peer)), ("defaultBannedRights", String(describing: defaultBannedRights)), ("version", String(describing: version))]) + return ("updateChatDefaultBannedRights", [("peer", peer as Any), ("defaultBannedRights", defaultBannedRights as Any), ("version", version as Any)]) case .updateChatParticipant(let flags, let chatId, let date, let actorId, let userId, let prevParticipant, let newParticipant, let invite, let qts): - return ("updateChatParticipant", [("flags", String(describing: flags)), ("chatId", String(describing: chatId)), ("date", String(describing: date)), ("actorId", String(describing: actorId)), ("userId", String(describing: userId)), ("prevParticipant", String(describing: prevParticipant)), ("newParticipant", String(describing: newParticipant)), ("invite", String(describing: invite)), ("qts", String(describing: qts))]) + return ("updateChatParticipant", [("flags", flags as Any), ("chatId", chatId as Any), ("date", date as Any), ("actorId", actorId as Any), ("userId", userId as Any), ("prevParticipant", prevParticipant as Any), ("newParticipant", newParticipant as Any), ("invite", invite as Any), ("qts", qts as Any)]) case .updateChatParticipantAdd(let chatId, let userId, let inviterId, let date, let version): - return ("updateChatParticipantAdd", [("chatId", String(describing: chatId)), ("userId", String(describing: userId)), ("inviterId", String(describing: inviterId)), ("date", String(describing: date)), ("version", String(describing: version))]) + return ("updateChatParticipantAdd", [("chatId", chatId as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any), ("version", version as Any)]) case .updateChatParticipantAdmin(let chatId, let userId, let isAdmin, let version): - return ("updateChatParticipantAdmin", [("chatId", String(describing: chatId)), ("userId", String(describing: userId)), ("isAdmin", String(describing: isAdmin)), ("version", String(describing: version))]) + return ("updateChatParticipantAdmin", [("chatId", chatId as Any), ("userId", userId as Any), ("isAdmin", isAdmin as Any), ("version", version as Any)]) case .updateChatParticipantDelete(let chatId, let userId, let version): - return ("updateChatParticipantDelete", [("chatId", String(describing: chatId)), ("userId", String(describing: userId)), ("version", String(describing: version))]) + return ("updateChatParticipantDelete", [("chatId", chatId as Any), ("userId", userId as Any), ("version", version as Any)]) case .updateChatParticipants(let participants): - return ("updateChatParticipants", [("participants", String(describing: participants))]) + return ("updateChatParticipants", [("participants", participants as Any)]) case .updateChatUserTyping(let chatId, let fromId, let action): - return ("updateChatUserTyping", [("chatId", String(describing: chatId)), ("fromId", String(describing: fromId)), ("action", String(describing: action))]) + return ("updateChatUserTyping", [("chatId", chatId as Any), ("fromId", fromId as Any), ("action", action as Any)]) case .updateConfig: return ("updateConfig", []) case .updateContactsReset: return ("updateContactsReset", []) case .updateDcOptions(let dcOptions): - return ("updateDcOptions", [("dcOptions", String(describing: dcOptions))]) + return ("updateDcOptions", [("dcOptions", dcOptions as Any)]) case .updateDeleteChannelMessages(let channelId, let messages, let pts, let ptsCount): - return ("updateDeleteChannelMessages", [("channelId", String(describing: channelId)), ("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateDeleteChannelMessages", [("channelId", channelId as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateDeleteMessages(let messages, let pts, let ptsCount): - return ("updateDeleteMessages", [("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateDeleteMessages", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateDeleteScheduledMessages(let peer, let messages): - return ("updateDeleteScheduledMessages", [("peer", String(describing: peer)), ("messages", String(describing: messages))]) + return ("updateDeleteScheduledMessages", [("peer", peer as Any), ("messages", messages as Any)]) case .updateDialogFilter(let flags, let id, let filter): - return ("updateDialogFilter", [("flags", String(describing: flags)), ("id", String(describing: id)), ("filter", String(describing: filter))]) + return ("updateDialogFilter", [("flags", flags as Any), ("id", id as Any), ("filter", filter as Any)]) case .updateDialogFilterOrder(let order): - return ("updateDialogFilterOrder", [("order", String(describing: order))]) + return ("updateDialogFilterOrder", [("order", order as Any)]) case .updateDialogFilters: return ("updateDialogFilters", []) case .updateDialogPinned(let flags, let folderId, let peer): - return ("updateDialogPinned", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("peer", String(describing: peer))]) + return ("updateDialogPinned", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any)]) case .updateDialogUnreadMark(let flags, let peer): - return ("updateDialogUnreadMark", [("flags", String(describing: flags)), ("peer", String(describing: peer))]) + return ("updateDialogUnreadMark", [("flags", flags as Any), ("peer", peer as Any)]) case .updateDraftMessage(let flags, let peer, let topMsgId, let draft): - return ("updateDraftMessage", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId)), ("draft", String(describing: draft))]) + return ("updateDraftMessage", [("flags", flags as Any), ("peer", peer as Any), ("topMsgId", topMsgId as Any), ("draft", draft as Any)]) case .updateEditChannelMessage(let message, let pts, let ptsCount): - return ("updateEditChannelMessage", [("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateEditChannelMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateEditMessage(let message, let pts, let ptsCount): - return ("updateEditMessage", [("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateEditMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateEncryptedChatTyping(let chatId): - return ("updateEncryptedChatTyping", [("chatId", String(describing: chatId))]) + return ("updateEncryptedChatTyping", [("chatId", chatId as Any)]) case .updateEncryptedMessagesRead(let chatId, let maxDate, let date): - return ("updateEncryptedMessagesRead", [("chatId", String(describing: chatId)), ("maxDate", String(describing: maxDate)), ("date", String(describing: date))]) + return ("updateEncryptedMessagesRead", [("chatId", chatId as Any), ("maxDate", maxDate as Any), ("date", date as Any)]) case .updateEncryption(let chat, let date): - return ("updateEncryption", [("chat", String(describing: chat)), ("date", String(describing: date))]) + return ("updateEncryption", [("chat", chat as Any), ("date", date as Any)]) case .updateFavedStickers: return ("updateFavedStickers", []) case .updateFolderPeers(let folderPeers, let pts, let ptsCount): - return ("updateFolderPeers", [("folderPeers", String(describing: folderPeers)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateFolderPeers", [("folderPeers", folderPeers as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateGeoLiveViewed(let peer, let msgId): - return ("updateGeoLiveViewed", [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]) + return ("updateGeoLiveViewed", [("peer", peer as Any), ("msgId", msgId as Any)]) case .updateGroupCall(let chatId, let call): - return ("updateGroupCall", [("chatId", String(describing: chatId)), ("call", String(describing: call))]) + return ("updateGroupCall", [("chatId", chatId as Any), ("call", call as Any)]) case .updateGroupCallConnection(let flags, let params): - return ("updateGroupCallConnection", [("flags", String(describing: flags)), ("params", String(describing: params))]) + return ("updateGroupCallConnection", [("flags", flags as Any), ("params", params as Any)]) case .updateGroupCallParticipants(let call, let participants, let version): - return ("updateGroupCallParticipants", [("call", String(describing: call)), ("participants", String(describing: participants)), ("version", String(describing: version))]) + return ("updateGroupCallParticipants", [("call", call as Any), ("participants", participants as Any), ("version", version as Any)]) case .updateInlineBotCallbackQuery(let flags, let queryId, let userId, let msgId, let chatInstance, let data, let gameShortName): - return ("updateInlineBotCallbackQuery", [("flags", String(describing: flags)), ("queryId", String(describing: queryId)), ("userId", String(describing: userId)), ("msgId", String(describing: msgId)), ("chatInstance", String(describing: chatInstance)), ("data", String(describing: data)), ("gameShortName", String(describing: gameShortName))]) + return ("updateInlineBotCallbackQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("msgId", msgId as Any), ("chatInstance", chatInstance as Any), ("data", data as Any), ("gameShortName", gameShortName as Any)]) case .updateLangPack(let difference): - return ("updateLangPack", [("difference", String(describing: difference))]) + return ("updateLangPack", [("difference", difference as Any)]) case .updateLangPackTooLong(let langCode): - return ("updateLangPackTooLong", [("langCode", String(describing: langCode))]) + return ("updateLangPackTooLong", [("langCode", langCode as Any)]) case .updateLoginToken: return ("updateLoginToken", []) case .updateMessageExtendedMedia(let peer, let msgId, let extendedMedia): - return ("updateMessageExtendedMedia", [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("extendedMedia", String(describing: extendedMedia))]) + return ("updateMessageExtendedMedia", [("peer", peer as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any)]) case .updateMessageID(let id, let randomId): - return ("updateMessageID", [("id", String(describing: id)), ("randomId", String(describing: randomId))]) + return ("updateMessageID", [("id", id as Any), ("randomId", randomId as Any)]) case .updateMessagePoll(let flags, let pollId, let poll, let results): - return ("updateMessagePoll", [("flags", String(describing: flags)), ("pollId", String(describing: pollId)), ("poll", String(describing: poll)), ("results", String(describing: results))]) + return ("updateMessagePoll", [("flags", flags as Any), ("pollId", pollId as Any), ("poll", poll as Any), ("results", results as Any)]) case .updateMessagePollVote(let pollId, let userId, let options, let qts): - return ("updateMessagePollVote", [("pollId", String(describing: pollId)), ("userId", String(describing: userId)), ("options", String(describing: options)), ("qts", String(describing: qts))]) + return ("updateMessagePollVote", [("pollId", pollId as Any), ("userId", userId as Any), ("options", options as Any), ("qts", qts as Any)]) case .updateMessageReactions(let flags, let peer, let msgId, let topMsgId, let reactions): - return ("updateMessageReactions", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("topMsgId", String(describing: topMsgId)), ("reactions", String(describing: reactions))]) + return ("updateMessageReactions", [("flags", flags as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("topMsgId", topMsgId as Any), ("reactions", reactions as Any)]) case .updateMoveStickerSetToTop(let flags, let stickerset): - return ("updateMoveStickerSetToTop", [("flags", String(describing: flags)), ("stickerset", String(describing: stickerset))]) + return ("updateMoveStickerSetToTop", [("flags", flags as Any), ("stickerset", stickerset as Any)]) case .updateNewChannelMessage(let message, let pts, let ptsCount): - return ("updateNewChannelMessage", [("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateNewChannelMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateNewEncryptedMessage(let message, let qts): - return ("updateNewEncryptedMessage", [("message", String(describing: message)), ("qts", String(describing: qts))]) + return ("updateNewEncryptedMessage", [("message", message as Any), ("qts", qts as Any)]) case .updateNewMessage(let message, let pts, let ptsCount): - return ("updateNewMessage", [("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateNewMessage", [("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateNewScheduledMessage(let message): - return ("updateNewScheduledMessage", [("message", String(describing: message))]) + return ("updateNewScheduledMessage", [("message", message as Any)]) case .updateNewStickerSet(let stickerset): - return ("updateNewStickerSet", [("stickerset", String(describing: stickerset))]) + return ("updateNewStickerSet", [("stickerset", stickerset as Any)]) case .updateNotifySettings(let peer, let notifySettings): - return ("updateNotifySettings", [("peer", String(describing: peer)), ("notifySettings", String(describing: notifySettings))]) + return ("updateNotifySettings", [("peer", peer as Any), ("notifySettings", notifySettings as Any)]) case .updatePeerBlocked(let peerId, let blocked): - return ("updatePeerBlocked", [("peerId", String(describing: peerId)), ("blocked", String(describing: blocked))]) + return ("updatePeerBlocked", [("peerId", peerId as Any), ("blocked", blocked as Any)]) case .updatePeerHistoryTTL(let flags, let peer, let ttlPeriod): - return ("updatePeerHistoryTTL", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("updatePeerHistoryTTL", [("flags", flags as Any), ("peer", peer as Any), ("ttlPeriod", ttlPeriod as Any)]) case .updatePeerLocated(let peers): - return ("updatePeerLocated", [("peers", String(describing: peers))]) + return ("updatePeerLocated", [("peers", peers as Any)]) case .updatePeerSettings(let peer, let settings): - return ("updatePeerSettings", [("peer", String(describing: peer)), ("settings", String(describing: settings))]) + return ("updatePeerSettings", [("peer", peer as Any), ("settings", settings as Any)]) case .updatePendingJoinRequests(let peer, let requestsPending, let recentRequesters): - return ("updatePendingJoinRequests", [("peer", String(describing: peer)), ("requestsPending", String(describing: requestsPending)), ("recentRequesters", String(describing: recentRequesters))]) + return ("updatePendingJoinRequests", [("peer", peer as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any)]) case .updatePhoneCall(let phoneCall): - return ("updatePhoneCall", [("phoneCall", String(describing: phoneCall))]) + return ("updatePhoneCall", [("phoneCall", phoneCall as Any)]) case .updatePhoneCallSignalingData(let phoneCallId, let data): - return ("updatePhoneCallSignalingData", [("phoneCallId", String(describing: phoneCallId)), ("data", String(describing: data))]) + return ("updatePhoneCallSignalingData", [("phoneCallId", phoneCallId as Any), ("data", data as Any)]) case .updatePinnedChannelMessages(let flags, let channelId, let messages, let pts, let ptsCount): - return ("updatePinnedChannelMessages", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updatePinnedChannelMessages", [("flags", flags as Any), ("channelId", channelId as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updatePinnedDialogs(let flags, let folderId, let order): - return ("updatePinnedDialogs", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("order", String(describing: order))]) + return ("updatePinnedDialogs", [("flags", flags as Any), ("folderId", folderId as Any), ("order", order as Any)]) case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount): - return ("updatePinnedMessages", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updatePinnedMessages", [("flags", flags as Any), ("peer", peer as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updatePrivacy(let key, let rules): - return ("updatePrivacy", [("key", String(describing: key)), ("rules", String(describing: rules))]) + return ("updatePrivacy", [("key", key as Any), ("rules", rules as Any)]) case .updatePtsChanged: return ("updatePtsChanged", []) case .updateReadChannelDiscussionInbox(let flags, let channelId, let topMsgId, let readMaxId, let broadcastId, let broadcastPost): - return ("updateReadChannelDiscussionInbox", [("flags", String(describing: flags)), ("channelId", String(describing: channelId)), ("topMsgId", String(describing: topMsgId)), ("readMaxId", String(describing: readMaxId)), ("broadcastId", String(describing: broadcastId)), ("broadcastPost", String(describing: broadcastPost))]) + return ("updateReadChannelDiscussionInbox", [("flags", flags as Any), ("channelId", channelId as Any), ("topMsgId", topMsgId as Any), ("readMaxId", readMaxId as Any), ("broadcastId", broadcastId as Any), ("broadcastPost", broadcastPost as Any)]) case .updateReadChannelDiscussionOutbox(let channelId, let topMsgId, let readMaxId): - return ("updateReadChannelDiscussionOutbox", [("channelId", String(describing: channelId)), ("topMsgId", String(describing: topMsgId)), ("readMaxId", String(describing: readMaxId))]) + return ("updateReadChannelDiscussionOutbox", [("channelId", channelId as Any), ("topMsgId", topMsgId as Any), ("readMaxId", readMaxId as Any)]) case .updateReadChannelInbox(let flags, let folderId, let channelId, let maxId, let stillUnreadCount, let pts): - return ("updateReadChannelInbox", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("channelId", String(describing: channelId)), ("maxId", String(describing: maxId)), ("stillUnreadCount", String(describing: stillUnreadCount)), ("pts", String(describing: pts))]) + return ("updateReadChannelInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("channelId", channelId as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any)]) case .updateReadChannelOutbox(let channelId, let maxId): - return ("updateReadChannelOutbox", [("channelId", String(describing: channelId)), ("maxId", String(describing: maxId))]) + return ("updateReadChannelOutbox", [("channelId", channelId as Any), ("maxId", maxId as Any)]) case .updateReadFeaturedEmojiStickers: return ("updateReadFeaturedEmojiStickers", []) case .updateReadFeaturedStickers: return ("updateReadFeaturedStickers", []) case .updateReadHistoryInbox(let flags, let folderId, let peer, let maxId, let stillUnreadCount, let pts, let ptsCount): - return ("updateReadHistoryInbox", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("peer", String(describing: peer)), ("maxId", String(describing: maxId)), ("stillUnreadCount", String(describing: stillUnreadCount)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateReadHistoryInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateReadHistoryOutbox(let peer, let maxId, let pts, let ptsCount): - return ("updateReadHistoryOutbox", [("peer", String(describing: peer)), ("maxId", String(describing: maxId)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateReadHistoryOutbox", [("peer", peer as Any), ("maxId", maxId as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateReadMessagesContents(let messages, let pts, let ptsCount): - return ("updateReadMessagesContents", [("messages", String(describing: messages)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateReadMessagesContents", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateRecentEmojiStatuses: return ("updateRecentEmojiStatuses", []) case .updateRecentReactions: @@ -1911,31 +1930,31 @@ public extension Api { case .updateSavedRingtones: return ("updateSavedRingtones", []) case .updateServiceNotification(let flags, let inboxDate, let type, let message, let media, let entities): - return ("updateServiceNotification", [("flags", String(describing: flags)), ("inboxDate", String(describing: inboxDate)), ("type", String(describing: type)), ("message", String(describing: message)), ("media", String(describing: media)), ("entities", String(describing: entities))]) + return ("updateServiceNotification", [("flags", flags as Any), ("inboxDate", inboxDate as Any), ("type", type as Any), ("message", message as Any), ("media", media as Any), ("entities", entities as Any)]) case .updateStickerSets(let flags): - return ("updateStickerSets", [("flags", String(describing: flags))]) + return ("updateStickerSets", [("flags", flags as Any)]) case .updateStickerSetsOrder(let flags, let order): - return ("updateStickerSetsOrder", [("flags", String(describing: flags)), ("order", String(describing: order))]) + return ("updateStickerSetsOrder", [("flags", flags as Any), ("order", order as Any)]) case .updateTheme(let theme): - return ("updateTheme", [("theme", String(describing: theme))]) + return ("updateTheme", [("theme", theme as Any)]) case .updateTranscribedAudio(let flags, let peer, let msgId, let transcriptionId, let text): - return ("updateTranscribedAudio", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) + return ("updateTranscribedAudio", [("flags", flags as Any), ("peer", peer as Any), ("msgId", msgId as Any), ("transcriptionId", transcriptionId as Any), ("text", text as Any)]) + case .updateUser(let userId): + return ("updateUser", [("userId", userId as Any)]) case .updateUserEmojiStatus(let userId, let emojiStatus): - return ("updateUserEmojiStatus", [("userId", String(describing: userId)), ("emojiStatus", String(describing: emojiStatus))]) + return ("updateUserEmojiStatus", [("userId", userId as Any), ("emojiStatus", emojiStatus as Any)]) case .updateUserName(let userId, let firstName, let lastName, let usernames): - return ("updateUserName", [("userId", String(describing: userId)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("usernames", String(describing: usernames))]) + return ("updateUserName", [("userId", userId as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("usernames", usernames as Any)]) case .updateUserPhone(let userId, let phone): - return ("updateUserPhone", [("userId", String(describing: userId)), ("phone", String(describing: phone))]) - case .updateUserPhoto(let userId, let date, let photo, let previous): - return ("updateUserPhoto", [("userId", String(describing: userId)), ("date", String(describing: date)), ("photo", String(describing: photo)), ("previous", String(describing: previous))]) + return ("updateUserPhone", [("userId", userId as Any), ("phone", phone as Any)]) case .updateUserStatus(let userId, let status): - return ("updateUserStatus", [("userId", String(describing: userId)), ("status", String(describing: status))]) + return ("updateUserStatus", [("userId", userId as Any), ("status", status as Any)]) case .updateUserTyping(let userId, let action): - return ("updateUserTyping", [("userId", String(describing: userId)), ("action", String(describing: action))]) + return ("updateUserTyping", [("userId", userId as Any), ("action", action as Any)]) case .updateWebPage(let webpage, let pts, let ptsCount): - return ("updateWebPage", [("webpage", String(describing: webpage)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("updateWebPage", [("webpage", webpage as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) case .updateWebViewResultSent(let queryId): - return ("updateWebViewResultSent", [("queryId", String(describing: queryId))]) + return ("updateWebViewResultSent", [("queryId", queryId as Any)]) } } @@ -3711,6 +3730,17 @@ public extension Api { return nil } } + public static func parse_updateUser(_ reader: BufferReader) -> Update? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateUser(userId: _1!) + } + else { + return nil + } + } public static func parse_updateUserEmojiStatus(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() @@ -3763,30 +3793,6 @@ public extension Api { return nil } } - public static func parse_updateUserPhoto(_ reader: BufferReader) -> Update? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Api.UserProfilePhoto? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.UserProfilePhoto - } - var _4: Api.Bool? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Bool - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.Update.updateUserPhoto(userId: _1!, date: _2!, photo: _3!, previous: _4!) - } - else { - return nil - } - } public static func parse_updateUserStatus(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 1e7d3e5c543..b5885f7affa 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -134,17 +134,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .updateShort(let update, let date): - return ("updateShort", [("update", String(describing: update)), ("date", String(describing: date))]) + return ("updateShort", [("update", update as Any), ("date", date as Any)]) case .updateShortChatMessage(let flags, let id, let fromId, let chatId, let message, let pts, let ptsCount, let date, let fwdFrom, let viaBotId, let replyTo, let entities, let ttlPeriod): - return ("updateShortChatMessage", [("flags", String(describing: flags)), ("id", String(describing: id)), ("fromId", String(describing: fromId)), ("chatId", String(describing: chatId)), ("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount)), ("date", String(describing: date)), ("fwdFrom", String(describing: fwdFrom)), ("viaBotId", String(describing: viaBotId)), ("replyTo", String(describing: replyTo)), ("entities", String(describing: entities)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("updateShortChatMessage", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("chatId", chatId as Any), ("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any), ("date", date as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("entities", entities as Any), ("ttlPeriod", ttlPeriod as Any)]) case .updateShortMessage(let flags, let id, let userId, let message, let pts, let ptsCount, let date, let fwdFrom, let viaBotId, let replyTo, let entities, let ttlPeriod): - return ("updateShortMessage", [("flags", String(describing: flags)), ("id", String(describing: id)), ("userId", String(describing: userId)), ("message", String(describing: message)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount)), ("date", String(describing: date)), ("fwdFrom", String(describing: fwdFrom)), ("viaBotId", String(describing: viaBotId)), ("replyTo", String(describing: replyTo)), ("entities", String(describing: entities)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("updateShortMessage", [("flags", flags as Any), ("id", id as Any), ("userId", userId as Any), ("message", message as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any), ("date", date as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("replyTo", replyTo as Any), ("entities", entities as Any), ("ttlPeriod", ttlPeriod as Any)]) case .updateShortSentMessage(let flags, let id, let pts, let ptsCount, let date, let media, let entities, let ttlPeriod): - return ("updateShortSentMessage", [("flags", String(describing: flags)), ("id", String(describing: id)), ("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount)), ("date", String(describing: date)), ("media", String(describing: media)), ("entities", String(describing: entities)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("updateShortSentMessage", [("flags", flags as Any), ("id", id as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any), ("date", date as Any), ("media", media as Any), ("entities", entities as Any), ("ttlPeriod", ttlPeriod as Any)]) case .updates(let updates, let users, let chats, let date, let seq): - return ("updates", [("updates", String(describing: updates)), ("users", String(describing: users)), ("chats", String(describing: chats)), ("date", String(describing: date)), ("seq", String(describing: seq))]) + return ("updates", [("updates", updates as Any), ("users", users as Any), ("chats", chats as Any), ("date", date as Any), ("seq", seq as Any)]) case .updatesCombined(let updates, let users, let chats, let date, let seqStart, let seq): - return ("updatesCombined", [("updates", String(describing: updates)), ("users", String(describing: users)), ("chats", String(describing: chats)), ("date", String(describing: date)), ("seqStart", String(describing: seqStart)), ("seq", String(describing: seq))]) + return ("updatesCombined", [("updates", updates as Any), ("users", users as Any), ("chats", chats as Any), ("date", date as Any), ("seqStart", seqStart as Any), ("seq", seq as Any)]) case .updatesTooLong: return ("updatesTooLong", []) } @@ -406,11 +406,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .urlAuthResultAccepted(let url): - return ("urlAuthResultAccepted", [("url", String(describing: url))]) + return ("urlAuthResultAccepted", [("url", url as Any)]) case .urlAuthResultDefault: return ("urlAuthResultDefault", []) case .urlAuthResultRequest(let flags, let bot, let domain): - return ("urlAuthResultRequest", [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("domain", String(describing: domain))]) + return ("urlAuthResultRequest", [("flags", flags as Any), ("bot", bot as Any), ("domain", domain as Any)]) } } @@ -498,9 +498,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames): - return ("user", [("flags", String(describing: flags)), ("flags2", String(describing: flags2)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("username", String(describing: username)), ("phone", String(describing: phone)), ("photo", String(describing: photo)), ("status", String(describing: status)), ("botInfoVersion", String(describing: botInfoVersion)), ("restrictionReason", String(describing: restrictionReason)), ("botInlinePlaceholder", String(describing: botInlinePlaceholder)), ("langCode", String(describing: langCode)), ("emojiStatus", String(describing: emojiStatus)), ("usernames", String(describing: usernames))]) + return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any)]) case .userEmpty(let id): - return ("userEmpty", [("id", String(describing: id))]) + return ("userEmpty", [("id", id as Any)]) } } @@ -586,19 +586,21 @@ public extension Api { } public extension Api { enum UserFull: TypeConstructorDescription { - case userFull(flags: Int32, id: Int64, about: String?, settings: Api.PeerSettings, profilePhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?) + case userFull(flags: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .userFull(let flags, let id, let about, let settings, let profilePhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts): + case .userFull(let flags, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts): if boxed { - buffer.appendInt32(-994968513) + buffer.appendInt32(-120378643) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {serializeString(about!, buffer: buffer, boxed: false)} settings.serialize(buffer, true) + if Int(flags) & Int(1 << 21) != 0 {personalPhoto!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {profilePhoto!.serialize(buffer, true)} + if Int(flags) & Int(1 << 22) != 0 {fallbackPhoto!.serialize(buffer, true)} notifySettings.serialize(buffer, true) if Int(flags) & Int(1 << 3) != 0 {botInfo!.serialize(buffer, true)} if Int(flags) & Int(1 << 6) != 0 {serializeInt32(pinnedMsgId!, buffer: buffer, boxed: false)} @@ -620,8 +622,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .userFull(let flags, let id, let about, let settings, let profilePhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts): - return ("userFull", [("flags", String(describing: flags)), ("id", String(describing: id)), ("about", String(describing: about)), ("settings", String(describing: settings)), ("profilePhoto", String(describing: profilePhoto)), ("notifySettings", String(describing: notifySettings)), ("botInfo", String(describing: botInfo)), ("pinnedMsgId", String(describing: pinnedMsgId)), ("commonChatsCount", String(describing: commonChatsCount)), ("folderId", String(describing: folderId)), ("ttlPeriod", String(describing: ttlPeriod)), ("themeEmoticon", String(describing: themeEmoticon)), ("privateForwardName", String(describing: privateForwardName)), ("botGroupAdminRights", String(describing: botGroupAdminRights)), ("botBroadcastAdminRights", String(describing: botBroadcastAdminRights)), ("premiumGifts", String(describing: premiumGifts))]) + case .userFull(let flags, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts): + return ("userFull", [("flags", flags as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any)]) } } @@ -637,59 +639,69 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.PeerSettings } var _5: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 21) != 0 {if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Photo } } - var _6: Api.PeerNotifySettings? + var _6: Api.Photo? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _7: Api.Photo? + if Int(_1!) & Int(1 << 22) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Photo + } } + var _8: Api.PeerNotifySettings? if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings + _8 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } - var _7: Api.BotInfo? + var _9: Api.BotInfo? if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.BotInfo + _9 = Api.parse(reader, signature: signature) as? Api.BotInfo } } - var _8: Int32? - if Int(_1!) & Int(1 << 6) != 0 {_8 = reader.readInt32() } - var _9: Int32? - _9 = reader.readInt32() var _10: Int32? - if Int(_1!) & Int(1 << 11) != 0 {_10 = reader.readInt32() } + if Int(_1!) & Int(1 << 6) != 0 {_10 = reader.readInt32() } var _11: Int32? - if Int(_1!) & Int(1 << 14) != 0 {_11 = reader.readInt32() } - var _12: String? - if Int(_1!) & Int(1 << 15) != 0 {_12 = parseString(reader) } - var _13: String? - if Int(_1!) & Int(1 << 16) != 0 {_13 = parseString(reader) } - var _14: Api.ChatAdminRights? + _11 = reader.readInt32() + var _12: Int32? + if Int(_1!) & Int(1 << 11) != 0 {_12 = reader.readInt32() } + var _13: Int32? + if Int(_1!) & Int(1 << 14) != 0 {_13 = reader.readInt32() } + var _14: String? + if Int(_1!) & Int(1 << 15) != 0 {_14 = parseString(reader) } + var _15: String? + if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) } + var _16: Api.ChatAdminRights? if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() { - _14 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights + _16 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } - var _15: Api.ChatAdminRights? + var _17: Api.ChatAdminRights? if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { - _15 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights + _17 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } - var _16: [Api.PremiumGiftOption]? + var _18: [Api.PremiumGiftOption]? if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() { - _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftOption.self) + _18 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PremiumGiftOption.self) } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 14) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 15) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 16) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 17) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 18) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 19) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.UserFull.userFull(flags: _1!, id: _2!, about: _3, settings: _4!, profilePhoto: _5, notifySettings: _6!, botInfo: _7, pinnedMsgId: _8, commonChatsCount: _9!, folderId: _10, ttlPeriod: _11, themeEmoticon: _12, privateForwardName: _13, botGroupAdminRights: _14, botBroadcastAdminRights: _15, premiumGifts: _16) + let _c5 = (Int(_1!) & Int(1 << 21) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 22) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 6) == 0) || _10 != nil + let _c11 = _11 != nil + let _c12 = (Int(_1!) & Int(1 << 11) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 14) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil + let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil + let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil + let _c17 = (Int(_1!) & Int(1 << 18) == 0) || _17 != nil + let _c18 = (Int(_1!) & Int(1 << 19) == 0) || _18 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 { + return Api.UserFull.userFull(flags: _1!, id: _2!, about: _3, settings: _4!, personalPhoto: _5, profilePhoto: _6, fallbackPhoto: _7, notifySettings: _8!, botInfo: _9, pinnedMsgId: _10, commonChatsCount: _11!, folderId: _12, ttlPeriod: _13, themeEmoticon: _14, privateForwardName: _15, botGroupAdminRights: _16, botBroadcastAdminRights: _17, premiumGifts: _18) } else { return nil @@ -726,7 +738,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .userProfilePhoto(let flags, let photoId, let strippedThumb, let dcId): - return ("userProfilePhoto", [("flags", String(describing: flags)), ("photoId", String(describing: photoId)), ("strippedThumb", String(describing: strippedThumb)), ("dcId", String(describing: dcId))]) + return ("userProfilePhoto", [("flags", flags as Any), ("photoId", photoId as Any), ("strippedThumb", strippedThumb as Any), ("dcId", dcId as Any)]) case .userProfilePhotoEmpty: return ("userProfilePhotoEmpty", []) } @@ -817,9 +829,9 @@ public extension Api { case .userStatusLastWeek: return ("userStatusLastWeek", []) case .userStatusOffline(let wasOnline): - return ("userStatusOffline", [("wasOnline", String(describing: wasOnline))]) + return ("userStatusOffline", [("wasOnline", wasOnline as Any)]) case .userStatusOnline(let expires): - return ("userStatusOnline", [("expires", String(describing: expires))]) + return ("userStatusOnline", [("expires", expires as Any)]) case .userStatusRecently: return ("userStatusRecently", []) } @@ -881,7 +893,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .username(let flags, let username): - return ("username", [("flags", String(describing: flags)), ("username", String(describing: username))]) + return ("username", [("flags", flags as Any), ("username", username as Any)]) } } @@ -925,7 +937,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .videoSize(let flags, let type, let w, let h, let size, let videoStartTs): - return ("videoSize", [("flags", String(describing: flags)), ("type", String(describing: type)), ("w", String(describing: w)), ("h", String(describing: h)), ("size", String(describing: size)), ("videoStartTs", String(describing: videoStartTs))]) + return ("videoSize", [("flags", flags as Any), ("type", type as Any), ("w", w as Any), ("h", h as Any), ("size", size as Any), ("videoStartTs", videoStartTs as Any)]) } } @@ -990,9 +1002,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .wallPaper(let id, let flags, let accessHash, let slug, let document, let settings): - return ("wallPaper", [("id", String(describing: id)), ("flags", String(describing: flags)), ("accessHash", String(describing: accessHash)), ("slug", String(describing: slug)), ("document", String(describing: document)), ("settings", String(describing: settings))]) + return ("wallPaper", [("id", id as Any), ("flags", flags as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("document", document as Any), ("settings", settings as Any)]) case .wallPaperNoFile(let id, let flags, let settings): - return ("wallPaperNoFile", [("id", String(describing: id)), ("flags", String(describing: flags)), ("settings", String(describing: settings))]) + return ("wallPaperNoFile", [("id", id as Any), ("flags", flags as Any), ("settings", settings as Any)]) } } @@ -1072,7 +1084,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation): - return ("wallPaperSettings", [("flags", String(describing: flags)), ("backgroundColor", String(describing: backgroundColor)), ("secondBackgroundColor", String(describing: secondBackgroundColor)), ("thirdBackgroundColor", String(describing: thirdBackgroundColor)), ("fourthBackgroundColor", String(describing: fourthBackgroundColor)), ("intensity", String(describing: intensity)), ("rotation", String(describing: rotation))]) + return ("wallPaperSettings", [("flags", flags as Any), ("backgroundColor", backgroundColor as Any), ("secondBackgroundColor", secondBackgroundColor as Any), ("thirdBackgroundColor", thirdBackgroundColor as Any), ("fourthBackgroundColor", fourthBackgroundColor as Any), ("intensity", intensity as Any), ("rotation", rotation as Any)]) } } @@ -1134,7 +1146,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webAuthorization(let hash, let botId, let domain, let browser, let platform, let dateCreated, let dateActive, let ip, let region): - return ("webAuthorization", [("hash", String(describing: hash)), ("botId", String(describing: botId)), ("domain", String(describing: domain)), ("browser", String(describing: browser)), ("platform", String(describing: platform)), ("dateCreated", String(describing: dateCreated)), ("dateActive", String(describing: dateActive)), ("ip", String(describing: ip)), ("region", String(describing: region))]) + return ("webAuthorization", [("hash", hash as Any), ("botId", botId as Any), ("domain", domain as Any), ("browser", browser as Any), ("platform", platform as Any), ("dateCreated", dateCreated as Any), ("dateActive", dateActive as Any), ("ip", ip as Any), ("region", region as Any)]) } } @@ -1216,9 +1228,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webDocument(let url, let accessHash, let size, let mimeType, let attributes): - return ("webDocument", [("url", String(describing: url)), ("accessHash", String(describing: accessHash)), ("size", String(describing: size)), ("mimeType", String(describing: mimeType)), ("attributes", String(describing: attributes))]) + return ("webDocument", [("url", url as Any), ("accessHash", accessHash as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) case .webDocumentNoProxy(let url, let size, let mimeType, let attributes): - return ("webDocumentNoProxy", [("url", String(describing: url)), ("size", String(describing: size)), ("mimeType", String(describing: mimeType)), ("attributes", String(describing: attributes))]) + return ("webDocumentNoProxy", [("url", url as Any), ("size", size as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any)]) } } @@ -1335,13 +1347,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webPage(let flags, let id, let url, let displayUrl, let hash, let type, let siteName, let title, let description, let photo, let embedUrl, let embedType, let embedWidth, let embedHeight, let duration, let author, let document, let cachedPage, let attributes): - return ("webPage", [("flags", String(describing: flags)), ("id", String(describing: id)), ("url", String(describing: url)), ("displayUrl", String(describing: displayUrl)), ("hash", String(describing: hash)), ("type", String(describing: type)), ("siteName", String(describing: siteName)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("embedUrl", String(describing: embedUrl)), ("embedType", String(describing: embedType)), ("embedWidth", String(describing: embedWidth)), ("embedHeight", String(describing: embedHeight)), ("duration", String(describing: duration)), ("author", String(describing: author)), ("document", String(describing: document)), ("cachedPage", String(describing: cachedPage)), ("attributes", String(describing: attributes))]) + return ("webPage", [("flags", flags as Any), ("id", id as Any), ("url", url as Any), ("displayUrl", displayUrl as Any), ("hash", hash as Any), ("type", type as Any), ("siteName", siteName as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("embedUrl", embedUrl as Any), ("embedType", embedType as Any), ("embedWidth", embedWidth as Any), ("embedHeight", embedHeight as Any), ("duration", duration as Any), ("author", author as Any), ("document", document as Any), ("cachedPage", cachedPage as Any), ("attributes", attributes as Any)]) case .webPageEmpty(let id): - return ("webPageEmpty", [("id", String(describing: id))]) + return ("webPageEmpty", [("id", id as Any)]) case .webPageNotModified(let flags, let cachedPageViews): - return ("webPageNotModified", [("flags", String(describing: flags)), ("cachedPageViews", String(describing: cachedPageViews))]) + return ("webPageNotModified", [("flags", flags as Any), ("cachedPageViews", cachedPageViews as Any)]) case .webPagePending(let id, let date): - return ("webPagePending", [("id", String(describing: id)), ("date", String(describing: date))]) + return ("webPagePending", [("id", id as Any), ("date", date as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index ac607defc42..1e09defc193 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -22,7 +22,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webPageAttributeTheme(let flags, let documents, let settings): - return ("webPageAttributeTheme", [("flags", String(describing: flags)), ("documents", String(describing: documents)), ("settings", String(describing: settings))]) + return ("webPageAttributeTheme", [("flags", flags as Any), ("documents", documents as Any), ("settings", settings as Any)]) } } @@ -69,7 +69,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webViewMessageSent(let flags, let msgId): - return ("webViewMessageSent", [("flags", String(describing: flags)), ("msgId", String(describing: msgId))]) + return ("webViewMessageSent", [("flags", flags as Any), ("msgId", msgId as Any)]) } } @@ -111,7 +111,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webViewResultUrl(let queryId, let url): - return ("webViewResultUrl", [("queryId", String(describing: queryId)), ("url", String(describing: url))]) + return ("webViewResultUrl", [("queryId", queryId as Any), ("url", url as Any)]) } } @@ -171,7 +171,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .authorizationForm(let flags, let requiredTypes, let values, let errors, let users, let privacyPolicyUrl): - return ("authorizationForm", [("flags", String(describing: flags)), ("requiredTypes", String(describing: requiredTypes)), ("values", String(describing: values)), ("errors", String(describing: errors)), ("users", String(describing: users)), ("privacyPolicyUrl", String(describing: privacyPolicyUrl))]) + return ("authorizationForm", [("flags", flags as Any), ("requiredTypes", requiredTypes as Any), ("values", values as Any), ("errors", errors as Any), ("users", users as Any), ("privacyPolicyUrl", privacyPolicyUrl as Any)]) } } @@ -235,7 +235,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .authorizations(let authorizationTtlDays, let authorizations): - return ("authorizations", [("authorizationTtlDays", String(describing: authorizationTtlDays)), ("authorizations", String(describing: authorizations))]) + return ("authorizations", [("authorizationTtlDays", authorizationTtlDays as Any), ("authorizations", authorizations as Any)]) } } @@ -278,7 +278,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .autoDownloadSettings(let low, let medium, let high): - return ("autoDownloadSettings", [("low", String(describing: low)), ("medium", String(describing: medium)), ("high", String(describing: high))]) + return ("autoDownloadSettings", [("low", low as Any), ("medium", medium as Any), ("high", high as Any)]) } } @@ -326,7 +326,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .contentSettings(let flags): - return ("contentSettings", [("flags", String(describing: flags))]) + return ("contentSettings", [("flags", flags as Any)]) } } @@ -370,9 +370,9 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emailVerified(let email): - return ("emailVerified", [("email", String(describing: email))]) + return ("emailVerified", [("email", email as Any)]) case .emailVerifiedLogin(let email, let sentCode): - return ("emailVerifiedLogin", [("email", String(describing: email)), ("sentCode", String(describing: sentCode))]) + return ("emailVerifiedLogin", [("email", email as Any), ("sentCode", sentCode as Any)]) } } @@ -436,7 +436,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiStatuses(let hash, let statuses): - return ("emojiStatuses", [("hash", String(describing: hash)), ("statuses", String(describing: statuses))]) + return ("emojiStatuses", [("hash", hash as Any), ("statuses", statuses as Any)]) case .emojiStatusesNotModified: return ("emojiStatusesNotModified", []) } @@ -492,7 +492,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .password(let flags, let currentAlgo, let srpB, let srpId, let hint, let emailUnconfirmedPattern, let newAlgo, let newSecureAlgo, let secureRandom, let pendingResetDate, let loginEmailPattern): - return ("password", [("flags", String(describing: flags)), ("currentAlgo", String(describing: currentAlgo)), ("srpB", String(describing: srpB)), ("srpId", String(describing: srpId)), ("hint", String(describing: hint)), ("emailUnconfirmedPattern", String(describing: emailUnconfirmedPattern)), ("newAlgo", String(describing: newAlgo)), ("newSecureAlgo", String(describing: newSecureAlgo)), ("secureRandom", String(describing: secureRandom)), ("pendingResetDate", String(describing: pendingResetDate)), ("loginEmailPattern", String(describing: loginEmailPattern))]) + return ("password", [("flags", flags as Any), ("currentAlgo", currentAlgo as Any), ("srpB", srpB as Any), ("srpId", srpId as Any), ("hint", hint as Any), ("emailUnconfirmedPattern", emailUnconfirmedPattern as Any), ("newAlgo", newAlgo as Any), ("newSecureAlgo", newSecureAlgo as Any), ("secureRandom", secureRandom as Any), ("pendingResetDate", pendingResetDate as Any), ("loginEmailPattern", loginEmailPattern as Any)]) } } @@ -569,7 +569,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .passwordInputSettings(let flags, let newAlgo, let newPasswordHash, let hint, let email, let newSecureSettings): - return ("passwordInputSettings", [("flags", String(describing: flags)), ("newAlgo", String(describing: newAlgo)), ("newPasswordHash", String(describing: newPasswordHash)), ("hint", String(describing: hint)), ("email", String(describing: email)), ("newSecureSettings", String(describing: newSecureSettings))]) + return ("passwordInputSettings", [("flags", flags as Any), ("newAlgo", newAlgo as Any), ("newPasswordHash", newPasswordHash as Any), ("hint", hint as Any), ("email", email as Any), ("newSecureSettings", newSecureSettings as Any)]) } } @@ -626,7 +626,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .passwordSettings(let flags, let email, let secureSettings): - return ("passwordSettings", [("flags", String(describing: flags)), ("email", String(describing: email)), ("secureSettings", String(describing: secureSettings))]) + return ("passwordSettings", [("flags", flags as Any), ("email", email as Any), ("secureSettings", secureSettings as Any)]) } } @@ -684,7 +684,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .privacyRules(let rules, let chats, let users): - return ("privacyRules", [("rules", String(describing: rules)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("privacyRules", [("rules", rules as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -746,11 +746,11 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .resetPasswordFailedWait(let retryDate): - return ("resetPasswordFailedWait", [("retryDate", String(describing: retryDate))]) + return ("resetPasswordFailedWait", [("retryDate", retryDate as Any)]) case .resetPasswordOk: return ("resetPasswordOk", []) case .resetPasswordRequestedWait(let untilDate): - return ("resetPasswordRequestedWait", [("untilDate", String(describing: untilDate))]) + return ("resetPasswordRequestedWait", [("untilDate", untilDate as Any)]) } } @@ -809,7 +809,7 @@ public extension Api.account { case .savedRingtone: return ("savedRingtone", []) case .savedRingtoneConverted(let document): - return ("savedRingtoneConverted", [("document", String(describing: document))]) + return ("savedRingtoneConverted", [("document", document as Any)]) } } @@ -862,7 +862,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .savedRingtones(let hash, let ringtones): - return ("savedRingtones", [("hash", String(describing: hash)), ("ringtones", String(describing: ringtones))]) + return ("savedRingtones", [("hash", hash as Any), ("ringtones", ringtones as Any)]) case .savedRingtonesNotModified: return ("savedRingtonesNotModified", []) } @@ -909,7 +909,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sentEmailCode(let emailPattern, let length): - return ("sentEmailCode", [("emailPattern", String(describing: emailPattern)), ("length", String(describing: length))]) + return ("sentEmailCode", [("emailPattern", emailPattern as Any), ("length", length as Any)]) } } @@ -948,7 +948,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .takeout(let id): - return ("takeout", [("id", String(describing: id))]) + return ("takeout", [("id", id as Any)]) } } @@ -996,7 +996,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .themes(let hash, let themes): - return ("themes", [("hash", String(describing: hash)), ("themes", String(describing: themes))]) + return ("themes", [("hash", hash as Any), ("themes", themes as Any)]) case .themesNotModified: return ("themesNotModified", []) } @@ -1043,7 +1043,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .tmpPassword(let tmpPassword, let validUntil): - return ("tmpPassword", [("tmpPassword", String(describing: tmpPassword)), ("validUntil", String(describing: validUntil))]) + return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)]) } } @@ -1094,7 +1094,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .wallPapers(let hash, let wallpapers): - return ("wallPapers", [("hash", String(describing: hash)), ("wallpapers", String(describing: wallpapers))]) + return ("wallPapers", [("hash", hash as Any), ("wallpapers", wallpapers as Any)]) case .wallPapersNotModified: return ("wallPapersNotModified", []) } @@ -1149,7 +1149,7 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webAuthorizations(let authorizations, let users): - return ("webAuthorizations", [("authorizations", String(describing: authorizations)), ("users", String(describing: users))]) + return ("webAuthorizations", [("authorizations", authorizations as Any), ("users", users as Any)]) } } @@ -1203,9 +1203,9 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .authorization(let flags, let otherwiseReloginDays, let tmpSessions, let user): - return ("authorization", [("flags", String(describing: flags)), ("otherwiseReloginDays", String(describing: otherwiseReloginDays)), ("tmpSessions", String(describing: tmpSessions)), ("user", String(describing: user))]) + return ("authorization", [("flags", flags as Any), ("otherwiseReloginDays", otherwiseReloginDays as Any), ("tmpSessions", tmpSessions as Any), ("user", user as Any)]) case .authorizationSignUpRequired(let flags, let termsOfService): - return ("authorizationSignUpRequired", [("flags", String(describing: flags)), ("termsOfService", String(describing: termsOfService))]) + return ("authorizationSignUpRequired", [("flags", flags as Any), ("termsOfService", termsOfService as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index b84c84469b0..10408368cb4 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -93,7 +93,7 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedAuthorization(let id, let bytes): - return ("exportedAuthorization", [("id", String(describing: id)), ("bytes", String(describing: bytes))]) + return ("exportedAuthorization", [("id", id as Any), ("bytes", bytes as Any)]) } } @@ -133,7 +133,7 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .loggedOut(let flags, let futureAuthToken): - return ("loggedOut", [("flags", String(describing: flags)), ("futureAuthToken", String(describing: futureAuthToken))]) + return ("loggedOut", [("flags", flags as Any), ("futureAuthToken", futureAuthToken as Any)]) } } @@ -188,11 +188,11 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .loginToken(let expires, let token): - return ("loginToken", [("expires", String(describing: expires)), ("token", String(describing: token))]) + return ("loginToken", [("expires", expires as Any), ("token", token as Any)]) case .loginTokenMigrateTo(let dcId, let token): - return ("loginTokenMigrateTo", [("dcId", String(describing: dcId)), ("token", String(describing: token))]) + return ("loginTokenMigrateTo", [("dcId", dcId as Any), ("token", token as Any)]) case .loginTokenSuccess(let authorization): - return ("loginTokenSuccess", [("authorization", String(describing: authorization))]) + return ("loginTokenSuccess", [("authorization", authorization as Any)]) } } @@ -258,7 +258,7 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .passwordRecovery(let emailPattern): - return ("passwordRecovery", [("emailPattern", String(describing: emailPattern))]) + return ("passwordRecovery", [("emailPattern", emailPattern as Any)]) } } @@ -298,7 +298,7 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sentCode(let flags, let type, let phoneCodeHash, let nextType, let timeout): - return ("sentCode", [("flags", String(describing: flags)), ("type", String(describing: type)), ("phoneCodeHash", String(describing: phoneCodeHash)), ("nextType", String(describing: nextType)), ("timeout", String(describing: timeout))]) + return ("sentCode", [("flags", flags as Any), ("type", type as Any), ("phoneCodeHash", phoneCodeHash as Any), ("nextType", nextType as Any), ("timeout", timeout as Any)]) } } @@ -404,21 +404,21 @@ public extension Api.auth { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sentCodeTypeApp(let length): - return ("sentCodeTypeApp", [("length", String(describing: length))]) + return ("sentCodeTypeApp", [("length", length as Any)]) case .sentCodeTypeCall(let length): - return ("sentCodeTypeCall", [("length", String(describing: length))]) + return ("sentCodeTypeCall", [("length", length as Any)]) case .sentCodeTypeEmailCode(let flags, let emailPattern, let length, let nextPhoneLoginDate): - return ("sentCodeTypeEmailCode", [("flags", String(describing: flags)), ("emailPattern", String(describing: emailPattern)), ("length", String(describing: length)), ("nextPhoneLoginDate", String(describing: nextPhoneLoginDate))]) + return ("sentCodeTypeEmailCode", [("flags", flags as Any), ("emailPattern", emailPattern as Any), ("length", length as Any), ("nextPhoneLoginDate", nextPhoneLoginDate as Any)]) case .sentCodeTypeFlashCall(let pattern): - return ("sentCodeTypeFlashCall", [("pattern", String(describing: pattern))]) + return ("sentCodeTypeFlashCall", [("pattern", pattern as Any)]) case .sentCodeTypeFragmentSms(let url, let length): - return ("sentCodeTypeFragmentSms", [("url", String(describing: url)), ("length", String(describing: length))]) + return ("sentCodeTypeFragmentSms", [("url", url as Any), ("length", length as Any)]) case .sentCodeTypeMissedCall(let prefix, let length): - return ("sentCodeTypeMissedCall", [("prefix", String(describing: prefix)), ("length", String(describing: length))]) + return ("sentCodeTypeMissedCall", [("prefix", prefix as Any), ("length", length as Any)]) case .sentCodeTypeSetUpEmailRequired(let flags): - return ("sentCodeTypeSetUpEmailRequired", [("flags", String(describing: flags))]) + return ("sentCodeTypeSetUpEmailRequired", [("flags", flags as Any)]) case .sentCodeTypeSms(let length): - return ("sentCodeTypeSms", [("length", String(describing: length))]) + return ("sentCodeTypeSms", [("length", length as Any)]) } } @@ -560,7 +560,7 @@ public extension Api.channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .adminLogResults(let events, let chats, let users): - return ("adminLogResults", [("events", String(describing: events)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("adminLogResults", [("events", events as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -618,7 +618,7 @@ public extension Api.channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelParticipant(let participant, let chats, let users): - return ("channelParticipant", [("participant", String(describing: participant)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("channelParticipant", [("participant", participant as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -688,7 +688,7 @@ public extension Api.channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelParticipants(let count, let participants, let chats, let users): - return ("channelParticipants", [("count", String(describing: count)), ("participants", String(describing: participants)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("channelParticipants", [("count", count as Any), ("participants", participants as Any), ("chats", chats as Any), ("users", users as Any)]) case .channelParticipantsNotModified: return ("channelParticipantsNotModified", []) } @@ -758,7 +758,7 @@ public extension Api.channels { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sendAsPeers(let peers, let chats, let users): - return ("sendAsPeers", [("peers", String(describing: peers)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("sendAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -842,9 +842,9 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .blocked(let blocked, let chats, let users): - return ("blocked", [("blocked", String(describing: blocked)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("blocked", [("blocked", blocked as Any), ("chats", chats as Any), ("users", users as Any)]) case .blockedSlice(let count, let blocked, let chats, let users): - return ("blockedSlice", [("count", String(describing: count)), ("blocked", String(describing: blocked)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("blockedSlice", [("count", count as Any), ("blocked", blocked as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -935,7 +935,7 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .contacts(let contacts, let savedCount, let users): - return ("contacts", [("contacts", String(describing: contacts)), ("savedCount", String(describing: savedCount)), ("users", String(describing: users))]) + return ("contacts", [("contacts", contacts as Any), ("savedCount", savedCount as Any), ("users", users as Any)]) case .contactsNotModified: return ("contactsNotModified", []) } @@ -1005,7 +1005,7 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .found(let myResults, let results, let chats, let users): - return ("found", [("myResults", String(describing: myResults)), ("results", String(describing: results)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("found", [("myResults", myResults as Any), ("results", results as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1077,7 +1077,7 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .importedContacts(let imported, let popularInvites, let retryContacts, let users): - return ("importedContacts", [("imported", String(describing: imported)), ("popularInvites", String(describing: popularInvites)), ("retryContacts", String(describing: retryContacts)), ("users", String(describing: users))]) + return ("importedContacts", [("imported", imported as Any), ("popularInvites", popularInvites as Any), ("retryContacts", retryContacts as Any), ("users", users as Any)]) } } @@ -1140,7 +1140,7 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .resolvedPeer(let peer, let chats, let users): - return ("resolvedPeer", [("peer", String(describing: peer)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("resolvedPeer", [("peer", peer as Any), ("chats", chats as Any), ("users", users as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index fa5f9162c9e..0934ea5662f 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -44,7 +44,7 @@ public extension Api.contacts { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .topPeers(let categories, let chats, let users): - return ("topPeers", [("categories", String(describing: categories)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("topPeers", [("categories", categories as Any), ("chats", chats as Any), ("users", users as Any)]) case .topPeersDisabled: return ("topPeersDisabled", []) case .topPeersNotModified: @@ -120,7 +120,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .appUpdate(let flags, let id, let version, let text, let entities, let document, let url, let sticker): - return ("appUpdate", [("flags", String(describing: flags)), ("id", String(describing: id)), ("version", String(describing: version)), ("text", String(describing: text)), ("entities", String(describing: entities)), ("document", String(describing: document)), ("url", String(describing: url)), ("sticker", String(describing: sticker))]) + return ("appUpdate", [("flags", flags as Any), ("id", id as Any), ("version", version as Any), ("text", text as Any), ("entities", entities as Any), ("document", document as Any), ("url", url as Any), ("sticker", sticker as Any)]) case .noAppUpdate: return ("noAppUpdate", []) } @@ -200,7 +200,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .countriesList(let countries, let hash): - return ("countriesList", [("countries", String(describing: countries)), ("hash", String(describing: hash))]) + return ("countriesList", [("countries", countries as Any), ("hash", hash as Any)]) case .countriesListNotModified: return ("countriesListNotModified", []) } @@ -254,7 +254,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .country(let flags, let iso2, let defaultName, let name, let countryCodes): - return ("country", [("flags", String(describing: flags)), ("iso2", String(describing: iso2)), ("defaultName", String(describing: defaultName)), ("name", String(describing: name)), ("countryCodes", String(describing: countryCodes))]) + return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)]) } } @@ -315,7 +315,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .countryCode(let flags, let countryCode, let prefixes, let patterns): - return ("countryCode", [("flags", String(describing: flags)), ("countryCode", String(describing: countryCode)), ("prefixes", String(describing: prefixes)), ("patterns", String(describing: patterns))]) + return ("countryCode", [("flags", flags as Any), ("countryCode", countryCode as Any), ("prefixes", prefixes as Any), ("patterns", patterns as Any)]) } } @@ -377,7 +377,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .deepLinkInfo(let flags, let message, let entities): - return ("deepLinkInfo", [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities))]) + return ("deepLinkInfo", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any)]) case .deepLinkInfoEmpty: return ("deepLinkInfoEmpty", []) } @@ -426,7 +426,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inviteText(let message): - return ("inviteText", [("message", String(describing: message))]) + return ("inviteText", [("message", message as Any)]) } } @@ -470,7 +470,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .passportConfig(let hash, let countriesLangs): - return ("passportConfig", [("hash", String(describing: hash)), ("countriesLangs", String(describing: countriesLangs))]) + return ("passportConfig", [("hash", hash as Any), ("countriesLangs", countriesLangs as Any)]) case .passportConfigNotModified: return ("passportConfigNotModified", []) } @@ -541,7 +541,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .premiumPromo(let statusText, let statusEntities, let videoSections, let videos, let periodOptions, let users): - return ("premiumPromo", [("statusText", String(describing: statusText)), ("statusEntities", String(describing: statusEntities)), ("videoSections", String(describing: videoSections)), ("videos", String(describing: videos)), ("periodOptions", String(describing: periodOptions)), ("users", String(describing: users))]) + return ("premiumPromo", [("statusText", statusText as Any), ("statusEntities", statusEntities as Any), ("videoSections", videoSections as Any), ("videos", videos as Any), ("periodOptions", periodOptions as Any), ("users", users as Any)]) } } @@ -623,9 +623,9 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .promoData(let flags, let expires, let peer, let chats, let users, let psaType, let psaMessage): - return ("promoData", [("flags", String(describing: flags)), ("expires", String(describing: expires)), ("peer", String(describing: peer)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("psaType", String(describing: psaType)), ("psaMessage", String(describing: psaMessage))]) + return ("promoData", [("flags", flags as Any), ("expires", expires as Any), ("peer", peer as Any), ("chats", chats as Any), ("users", users as Any), ("psaType", psaType as Any), ("psaMessage", psaMessage as Any)]) case .promoDataEmpty(let expires): - return ("promoDataEmpty", [("expires", String(describing: expires))]) + return ("promoDataEmpty", [("expires", expires as Any)]) } } @@ -710,7 +710,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .recentMeUrls(let urls, let chats, let users): - return ("recentMeUrls", [("urls", String(describing: urls)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("recentMeUrls", [("urls", urls as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -759,7 +759,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .support(let phoneNumber, let user): - return ("support", [("phoneNumber", String(describing: phoneNumber)), ("user", String(describing: user))]) + return ("support", [("phoneNumber", phoneNumber as Any), ("user", user as Any)]) } } @@ -800,7 +800,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .supportName(let name): - return ("supportName", [("name", String(describing: name))]) + return ("supportName", [("name", name as Any)]) } } @@ -844,7 +844,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .termsOfService(let flags, let id, let text, let entities, let minAgeConfirm): - return ("termsOfService", [("flags", String(describing: flags)), ("id", String(describing: id)), ("text", String(describing: text)), ("entities", String(describing: entities)), ("minAgeConfirm", String(describing: minAgeConfirm))]) + return ("termsOfService", [("flags", flags as Any), ("id", id as Any), ("text", text as Any), ("entities", entities as Any), ("minAgeConfirm", minAgeConfirm as Any)]) } } @@ -904,9 +904,9 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .termsOfServiceUpdate(let expires, let termsOfService): - return ("termsOfServiceUpdate", [("expires", String(describing: expires)), ("termsOfService", String(describing: termsOfService))]) + return ("termsOfServiceUpdate", [("expires", expires as Any), ("termsOfService", termsOfService as Any)]) case .termsOfServiceUpdateEmpty(let expires): - return ("termsOfServiceUpdateEmpty", [("expires", String(describing: expires))]) + return ("termsOfServiceUpdateEmpty", [("expires", expires as Any)]) } } @@ -972,7 +972,7 @@ public extension Api.help { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .userInfo(let message, let entities, let author, let date): - return ("userInfo", [("message", String(describing: message)), ("entities", String(describing: entities)), ("author", String(describing: author)), ("date", String(describing: date))]) + return ("userInfo", [("message", message as Any), ("entities", entities as Any), ("author", author as Any), ("date", date as Any)]) case .userInfoEmpty: return ("userInfoEmpty", []) } @@ -1031,7 +1031,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .affectedFoundMessages(let pts, let ptsCount, let offset, let messages): - return ("affectedFoundMessages", [("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount)), ("offset", String(describing: offset)), ("messages", String(describing: messages))]) + return ("affectedFoundMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any), ("messages", messages as Any)]) } } @@ -1080,7 +1080,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .affectedHistory(let pts, let ptsCount, let offset): - return ("affectedHistory", [("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount)), ("offset", String(describing: offset))]) + return ("affectedHistory", [("pts", pts as Any), ("ptsCount", ptsCount as Any), ("offset", offset as Any)]) } } @@ -1123,7 +1123,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .affectedMessages(let pts, let ptsCount): - return ("affectedMessages", [("pts", String(describing: pts)), ("ptsCount", String(describing: ptsCount))]) + return ("affectedMessages", [("pts", pts as Any), ("ptsCount", ptsCount as Any)]) } } @@ -1174,7 +1174,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .allStickers(let hash, let sets): - return ("allStickers", [("hash", String(describing: hash)), ("sets", String(describing: sets))]) + return ("allStickers", [("hash", hash as Any), ("sets", sets as Any)]) case .allStickersNotModified: return ("allStickersNotModified", []) } @@ -1225,7 +1225,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .archivedStickers(let count, let sets): - return ("archivedStickers", [("count", String(describing: count)), ("sets", String(describing: sets))]) + return ("archivedStickers", [("count", count as Any), ("sets", sets as Any)]) } } @@ -1278,7 +1278,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .availableReactions(let hash, let reactions): - return ("availableReactions", [("hash", String(describing: hash)), ("reactions", String(describing: reactions))]) + return ("availableReactions", [("hash", hash as Any), ("reactions", reactions as Any)]) case .availableReactionsNotModified: return ("availableReactionsNotModified", []) } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 2be1f04d094..0dde8a5bff0 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -19,7 +19,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botCallbackAnswer(let flags, let message, let url, let cacheTime): - return ("botCallbackAnswer", [("flags", String(describing: flags)), ("message", String(describing: message)), ("url", String(describing: url)), ("cacheTime", String(describing: cacheTime))]) + return ("botCallbackAnswer", [("flags", flags as Any), ("message", message as Any), ("url", url as Any), ("cacheTime", cacheTime as Any)]) } } @@ -78,7 +78,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .botResults(let flags, let queryId, let nextOffset, let switchPm, let results, let cacheTime, let users): - return ("botResults", [("flags", String(describing: flags)), ("queryId", String(describing: queryId)), ("nextOffset", String(describing: nextOffset)), ("switchPm", String(describing: switchPm)), ("results", String(describing: results)), ("cacheTime", String(describing: cacheTime)), ("users", String(describing: users))]) + return ("botResults", [("flags", flags as Any), ("queryId", queryId as Any), ("nextOffset", nextOffset as Any), ("switchPm", switchPm as Any), ("results", results as Any), ("cacheTime", cacheTime as Any), ("users", users as Any)]) } } @@ -147,7 +147,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatAdminsWithInvites(let admins, let users): - return ("chatAdminsWithInvites", [("admins", String(describing: admins)), ("users", String(describing: users))]) + return ("chatAdminsWithInvites", [("admins", admins as Any), ("users", users as Any)]) } } @@ -200,7 +200,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatFull(let fullChat, let chats, let users): - return ("chatFull", [("fullChat", String(describing: fullChat)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("chatFull", [("fullChat", fullChat as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -258,7 +258,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatInviteImporters(let count, let importers, let users): - return ("chatInviteImporters", [("count", String(describing: count)), ("importers", String(describing: importers)), ("users", String(describing: users))]) + return ("chatInviteImporters", [("count", count as Any), ("importers", importers as Any), ("users", users as Any)]) } } @@ -320,9 +320,9 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chats(let chats): - return ("chats", [("chats", String(describing: chats))]) + return ("chats", [("chats", chats as Any)]) case .chatsSlice(let count, let chats): - return ("chatsSlice", [("count", String(describing: count)), ("chats", String(describing: chats))]) + return ("chatsSlice", [("count", count as Any), ("chats", chats as Any)]) } } @@ -376,7 +376,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .checkedHistoryImportPeer(let confirmText): - return ("checkedHistoryImportPeer", [("confirmText", String(describing: confirmText))]) + return ("checkedHistoryImportPeer", [("confirmText", confirmText as Any)]) } } @@ -422,9 +422,9 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dhConfig(let g, let p, let version, let random): - return ("dhConfig", [("g", String(describing: g)), ("p", String(describing: p)), ("version", String(describing: version)), ("random", String(describing: random))]) + return ("dhConfig", [("g", g as Any), ("p", p as Any), ("version", version as Any), ("random", random as Any)]) case .dhConfigNotModified(let random): - return ("dhConfigNotModified", [("random", String(describing: random))]) + return ("dhConfigNotModified", [("random", random as Any)]) } } @@ -533,11 +533,11 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dialogs(let dialogs, let messages, let chats, let users): - return ("dialogs", [("dialogs", String(describing: dialogs)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("dialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) case .dialogsNotModified(let count): - return ("dialogsNotModified", [("count", String(describing: count))]) + return ("dialogsNotModified", [("count", count as Any)]) case .dialogsSlice(let count, let dialogs, let messages, let chats, let users): - return ("dialogsSlice", [("count", String(describing: count)), ("dialogs", String(describing: dialogs)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("dialogsSlice", [("count", count as Any), ("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -651,7 +651,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .discussionMessage(let flags, let messages, let maxId, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chats, let users): - return ("discussionMessage", [("flags", String(describing: flags)), ("messages", String(describing: messages)), ("maxId", String(describing: maxId)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("discussionMessage", [("flags", flags as Any), ("messages", messages as Any), ("maxId", maxId as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -732,9 +732,9 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedChatInvite(let invite, let users): - return ("exportedChatInvite", [("invite", String(describing: invite)), ("users", String(describing: users))]) + return ("exportedChatInvite", [("invite", invite as Any), ("users", users as Any)]) case .exportedChatInviteReplaced(let invite, let newInvite, let users): - return ("exportedChatInviteReplaced", [("invite", String(describing: invite)), ("newInvite", String(describing: newInvite)), ("users", String(describing: users))]) + return ("exportedChatInviteReplaced", [("invite", invite as Any), ("newInvite", newInvite as Any), ("users", users as Any)]) } } @@ -810,7 +810,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedChatInvites(let count, let invites, let users): - return ("exportedChatInvites", [("count", String(describing: count)), ("invites", String(describing: invites)), ("users", String(describing: users))]) + return ("exportedChatInvites", [("count", count as Any), ("invites", invites as Any), ("users", users as Any)]) } } @@ -873,7 +873,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .favedStickers(let hash, let packs, let stickers): - return ("favedStickers", [("hash", String(describing: hash)), ("packs", String(describing: packs)), ("stickers", String(describing: stickers))]) + return ("favedStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any)]) case .favedStickersNotModified: return ("favedStickersNotModified", []) } @@ -943,9 +943,9 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .featuredStickers(let flags, let hash, let count, let sets, let unread): - return ("featuredStickers", [("flags", String(describing: flags)), ("hash", String(describing: hash)), ("count", String(describing: count)), ("sets", String(describing: sets)), ("unread", String(describing: unread))]) + return ("featuredStickers", [("flags", flags as Any), ("hash", hash as Any), ("count", count as Any), ("sets", sets as Any), ("unread", unread as Any)]) case .featuredStickersNotModified(let count): - return ("featuredStickersNotModified", [("count", String(describing: count))]) + return ("featuredStickersNotModified", [("count", count as Any)]) } } @@ -1030,7 +1030,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .forumTopics(let flags, let count, let topics, let messages, let chats, let users, let pts): - return ("forumTopics", [("flags", String(describing: flags)), ("count", String(describing: count)), ("topics", String(describing: topics)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("pts", String(describing: pts))]) + return ("forumTopics", [("flags", flags as Any), ("count", count as Any), ("topics", topics as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any), ("pts", pts as Any)]) } } @@ -1104,7 +1104,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .foundStickerSets(let hash, let sets): - return ("foundStickerSets", [("hash", String(describing: hash)), ("sets", String(describing: sets))]) + return ("foundStickerSets", [("hash", hash as Any), ("sets", sets as Any)]) case .foundStickerSetsNotModified: return ("foundStickerSetsNotModified", []) } @@ -1159,7 +1159,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .highScores(let scores, let users): - return ("highScores", [("scores", String(describing: scores)), ("users", String(describing: users))]) + return ("highScores", [("scores", scores as Any), ("users", users as Any)]) } } @@ -1202,7 +1202,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .historyImport(let id): - return ("historyImport", [("id", String(describing: id))]) + return ("historyImport", [("id", id as Any)]) } } @@ -1239,7 +1239,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .historyImportParsed(let flags, let title): - return ("historyImportParsed", [("flags", String(describing: flags)), ("title", String(describing: title))]) + return ("historyImportParsed", [("flags", flags as Any), ("title", title as Any)]) } } @@ -1292,7 +1292,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inactiveChats(let dates, let chats, let users): - return ("inactiveChats", [("dates", String(describing: dates)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("inactiveChats", [("dates", dates as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1340,7 +1340,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageEditData(let flags): - return ("messageEditData", [("flags", String(describing: flags))]) + return ("messageEditData", [("flags", flags as Any)]) } } @@ -1393,7 +1393,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - return ("messageReactionsList", [("flags", String(describing: flags)), ("count", String(describing: count)), ("reactions", String(describing: reactions)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("nextOffset", String(describing: nextOffset))]) + return ("messageReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } @@ -1464,7 +1464,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageViews(let views, let chats, let users): - return ("messageViews", [("views", String(describing: views)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("messageViews", [("views", views as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1588,13 +1588,13 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelMessages(let flags, let pts, let count, let offsetIdOffset, let messages, let topics, let chats, let users): - return ("channelMessages", [("flags", String(describing: flags)), ("pts", String(describing: pts)), ("count", String(describing: count)), ("offsetIdOffset", String(describing: offsetIdOffset)), ("messages", String(describing: messages)), ("topics", String(describing: topics)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("channelMessages", [("flags", flags as Any), ("pts", pts as Any), ("count", count as Any), ("offsetIdOffset", offsetIdOffset as Any), ("messages", messages as Any), ("topics", topics as Any), ("chats", chats as Any), ("users", users as Any)]) case .messages(let messages, let chats, let users): - return ("messages", [("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("messages", [("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) case .messagesNotModified(let count): - return ("messagesNotModified", [("count", String(describing: count))]) + return ("messagesNotModified", [("count", count as Any)]) case .messagesSlice(let flags, let count, let nextRate, let offsetIdOffset, let messages, let chats, let users): - return ("messagesSlice", [("flags", String(describing: flags)), ("count", String(describing: count)), ("nextRate", String(describing: nextRate)), ("offsetIdOffset", String(describing: offsetIdOffset)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("messagesSlice", [("flags", flags as Any), ("count", count as Any), ("nextRate", nextRate as Any), ("offsetIdOffset", offsetIdOffset as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 13950c1d1b0..04a7bd362cd 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -36,7 +36,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerDialogs(let dialogs, let messages, let chats, let users, let state): - return ("peerDialogs", [("dialogs", String(describing: dialogs)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("state", String(describing: state))]) + return ("peerDialogs", [("dialogs", dialogs as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any), ("state", state as Any)]) } } @@ -104,7 +104,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .peerSettings(let settings, let chats, let users): - return ("peerSettings", [("settings", String(describing: settings)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("peerSettings", [("settings", settings as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -164,7 +164,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .reactions(let hash, let reactions): - return ("reactions", [("hash", String(describing: hash)), ("reactions", String(describing: reactions))]) + return ("reactions", [("hash", hash as Any), ("reactions", reactions as Any)]) case .reactionsNotModified: return ("reactionsNotModified", []) } @@ -232,7 +232,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .recentStickers(let hash, let packs, let stickers, let dates): - return ("recentStickers", [("hash", String(describing: hash)), ("packs", String(describing: packs)), ("stickers", String(describing: stickers)), ("dates", String(describing: dates))]) + return ("recentStickers", [("hash", hash as Any), ("packs", packs as Any), ("stickers", stickers as Any), ("dates", dates as Any)]) case .recentStickersNotModified: return ("recentStickersNotModified", []) } @@ -300,7 +300,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .savedGifs(let hash, let gifs): - return ("savedGifs", [("hash", String(describing: hash)), ("gifs", String(describing: gifs))]) + return ("savedGifs", [("hash", hash as Any), ("gifs", gifs as Any)]) case .savedGifsNotModified: return ("savedGifsNotModified", []) } @@ -348,7 +348,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .searchCounter(let flags, let filter, let count): - return ("searchCounter", [("flags", String(describing: flags)), ("filter", String(describing: filter)), ("count", String(describing: count))]) + return ("searchCounter", [("flags", flags as Any), ("filter", filter as Any), ("count", count as Any)]) } } @@ -416,7 +416,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .searchResultsCalendar(let flags, let count, let minDate, let minMsgId, let offsetIdOffset, let periods, let messages, let chats, let users): - return ("searchResultsCalendar", [("flags", String(describing: flags)), ("count", String(describing: count)), ("minDate", String(describing: minDate)), ("minMsgId", String(describing: minMsgId)), ("offsetIdOffset", String(describing: offsetIdOffset)), ("periods", String(describing: periods)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("searchResultsCalendar", [("flags", flags as Any), ("count", count as Any), ("minDate", minDate as Any), ("minMsgId", minMsgId as Any), ("offsetIdOffset", offsetIdOffset as Any), ("periods", periods as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -489,7 +489,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .searchResultsPositions(let count, let positions): - return ("searchResultsPositions", [("count", String(describing: count)), ("positions", String(describing: positions))]) + return ("searchResultsPositions", [("count", count as Any), ("positions", positions as Any)]) } } @@ -538,9 +538,9 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sentEncryptedFile(let date, let file): - return ("sentEncryptedFile", [("date", String(describing: date)), ("file", String(describing: file))]) + return ("sentEncryptedFile", [("date", date as Any), ("file", file as Any)]) case .sentEncryptedMessage(let date): - return ("sentEncryptedMessage", [("date", String(describing: date))]) + return ("sentEncryptedMessage", [("date", date as Any)]) } } @@ -615,7 +615,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .sponsoredMessages(let flags, let postsBetween, let messages, let chats, let users): - return ("sponsoredMessages", [("flags", String(describing: flags)), ("postsBetween", String(describing: postsBetween)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("sponsoredMessages", [("flags", flags as Any), ("postsBetween", postsBetween as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) case .sponsoredMessagesEmpty: return ("sponsoredMessagesEmpty", []) } @@ -696,7 +696,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerSet(let set, let packs, let keywords, let documents): - return ("stickerSet", [("set", String(describing: set)), ("packs", String(describing: packs)), ("keywords", String(describing: keywords)), ("documents", String(describing: documents))]) + return ("stickerSet", [("set", set as Any), ("packs", packs as Any), ("keywords", keywords as Any), ("documents", documents as Any)]) case .stickerSetNotModified: return ("stickerSetNotModified", []) } @@ -765,7 +765,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickerSetInstallResultArchive(let sets): - return ("stickerSetInstallResultArchive", [("sets", String(describing: sets))]) + return ("stickerSetInstallResultArchive", [("sets", sets as Any)]) case .stickerSetInstallResultSuccess: return ("stickerSetInstallResultSuccess", []) } @@ -820,7 +820,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .stickers(let hash, let stickers): - return ("stickers", [("hash", String(describing: hash)), ("stickers", String(describing: stickers))]) + return ("stickers", [("hash", hash as Any), ("stickers", stickers as Any)]) case .stickersNotModified: return ("stickersNotModified", []) } @@ -868,7 +868,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .transcribedAudio(let flags, let transcriptionId, let text): - return ("transcribedAudio", [("flags", String(describing: flags)), ("transcriptionId", String(describing: transcriptionId)), ("text", String(describing: text))]) + return ("transcribedAudio", [("flags", flags as Any), ("transcriptionId", transcriptionId as Any), ("text", text as Any)]) } } @@ -919,7 +919,7 @@ public extension Api.messages { case .translateNoResult: return ("translateNoResult", []) case .translateResultText(let text): - return ("translateResultText", [("text", String(describing: text))]) + return ("translateResultText", [("text", text as Any)]) } } @@ -970,7 +970,7 @@ public extension Api.messages { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .votesList(let flags, let count, let votes, let users, let nextOffset): - return ("votesList", [("flags", String(describing: flags)), ("count", String(describing: count)), ("votes", String(describing: votes)), ("users", String(describing: users)), ("nextOffset", String(describing: nextOffset))]) + return ("votesList", [("flags", flags as Any), ("count", count as Any), ("votes", votes as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } @@ -1027,7 +1027,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .bankCardData(let title, let openUrls): - return ("bankCardData", [("title", String(describing: title)), ("openUrls", String(describing: openUrls))]) + return ("bankCardData", [("title", title as Any), ("openUrls", openUrls as Any)]) } } @@ -1068,7 +1068,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedInvoice(let url): - return ("exportedInvoice", [("url", String(describing: url))]) + return ("exportedInvoice", [("url", url as Any)]) } } @@ -1130,7 +1130,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): - return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("additionalMethods", String(describing: additionalMethods)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))]) + return ("paymentForm", [("flags", flags as Any), ("formId", formId as Any), ("botId", botId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("providerId", providerId as Any), ("url", url as Any), ("nativeProvider", nativeProvider as Any), ("nativeParams", nativeParams as Any), ("additionalMethods", additionalMethods as Any), ("savedInfo", savedInfo as Any), ("savedCredentials", savedCredentials as Any), ("users", users as Any)]) } } @@ -1240,7 +1240,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentReceipt(let flags, let date, let botId, let providerId, let title, let description, let photo, let invoice, let info, let shipping, let tipAmount, let currency, let totalAmount, let credentialsTitle, let users): - return ("paymentReceipt", [("flags", String(describing: flags)), ("date", String(describing: date)), ("botId", String(describing: botId)), ("providerId", String(describing: providerId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("info", String(describing: info)), ("shipping", String(describing: shipping)), ("tipAmount", String(describing: tipAmount)), ("currency", String(describing: currency)), ("totalAmount", String(describing: totalAmount)), ("credentialsTitle", String(describing: credentialsTitle)), ("users", String(describing: users))]) + return ("paymentReceipt", [("flags", flags as Any), ("date", date as Any), ("botId", botId as Any), ("providerId", providerId as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("info", info as Any), ("shipping", shipping as Any), ("tipAmount", tipAmount as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("credentialsTitle", credentialsTitle as Any), ("users", users as Any)]) } } @@ -1335,9 +1335,9 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .paymentResult(let updates): - return ("paymentResult", [("updates", String(describing: updates))]) + return ("paymentResult", [("updates", updates as Any)]) case .paymentVerificationNeeded(let url): - return ("paymentVerificationNeeded", [("url", String(describing: url))]) + return ("paymentVerificationNeeded", [("url", url as Any)]) } } @@ -1387,7 +1387,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .savedInfo(let flags, let savedInfo): - return ("savedInfo", [("flags", String(describing: flags)), ("savedInfo", String(describing: savedInfo))]) + return ("savedInfo", [("flags", flags as Any), ("savedInfo", savedInfo as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index f01e761167c..f6509a4c298 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -22,7 +22,7 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .validatedRequestedInfo(let flags, let id, let shippingOptions): - return ("validatedRequestedInfo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("shippingOptions", String(describing: shippingOptions))]) + return ("validatedRequestedInfo", [("flags", flags as Any), ("id", id as Any), ("shippingOptions", shippingOptions as Any)]) } } @@ -66,7 +66,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedGroupCallInvite(let link): - return ("exportedGroupCallInvite", [("link", String(describing: link))]) + return ("exportedGroupCallInvite", [("link", link as Any)]) } } @@ -118,7 +118,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCall(let call, let participants, let participantsNextOffset, let chats, let users): - return ("groupCall", [("call", String(describing: call)), ("participants", String(describing: participants)), ("participantsNextOffset", String(describing: participantsNextOffset)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("groupCall", [("call", call as Any), ("participants", participants as Any), ("participantsNextOffset", participantsNextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -178,7 +178,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallStreamChannels(let channels): - return ("groupCallStreamChannels", [("channels", String(describing: channels))]) + return ("groupCallStreamChannels", [("channels", channels as Any)]) } } @@ -217,7 +217,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallStreamRtmpUrl(let url, let key): - return ("groupCallStreamRtmpUrl", [("url", String(describing: url)), ("key", String(describing: key))]) + return ("groupCallStreamRtmpUrl", [("url", url as Any), ("key", key as Any)]) } } @@ -273,7 +273,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupParticipants(let count, let participants, let nextOffset, let chats, let users, let version): - return ("groupParticipants", [("count", String(describing: count)), ("participants", String(describing: participants)), ("nextOffset", String(describing: nextOffset)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("version", String(describing: version))]) + return ("groupParticipants", [("count", count as Any), ("participants", participants as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any), ("version", version as Any)]) } } @@ -344,7 +344,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .joinAsPeers(let peers, let chats, let users): - return ("joinAsPeers", [("peers", String(describing: peers)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("joinAsPeers", [("peers", peers as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -397,7 +397,7 @@ public extension Api.phone { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .phoneCall(let phoneCall, let users): - return ("phoneCall", [("phoneCall", String(describing: phoneCall)), ("users", String(describing: users))]) + return ("phoneCall", [("phoneCall", phoneCall as Any), ("users", users as Any)]) } } @@ -445,7 +445,7 @@ public extension Api.photos { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .photo(let photo, let users): - return ("photo", [("photo", String(describing: photo)), ("users", String(describing: users))]) + return ("photo", [("photo", photo as Any), ("users", users as Any)]) } } @@ -514,9 +514,9 @@ public extension Api.photos { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .photos(let photos, let users): - return ("photos", [("photos", String(describing: photos)), ("users", String(describing: users))]) + return ("photos", [("photos", photos as Any), ("users", users as Any)]) case .photosSlice(let count, let photos, let users): - return ("photosSlice", [("count", String(describing: count)), ("photos", String(describing: photos)), ("users", String(describing: users))]) + return ("photosSlice", [("count", count as Any), ("photos", photos as Any), ("users", users as Any)]) } } @@ -598,7 +598,7 @@ public extension Api.stats { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .broadcastStats(let period, let followers, let viewsPerPost, let sharesPerPost, let enabledNotifications, let growthGraph, let followersGraph, let muteGraph, let topHoursGraph, let interactionsGraph, let ivInteractionsGraph, let viewsBySourceGraph, let newFollowersBySourceGraph, let languagesGraph, let recentMessageInteractions): - return ("broadcastStats", [("period", String(describing: period)), ("followers", String(describing: followers)), ("viewsPerPost", String(describing: viewsPerPost)), ("sharesPerPost", String(describing: sharesPerPost)), ("enabledNotifications", String(describing: enabledNotifications)), ("growthGraph", String(describing: growthGraph)), ("followersGraph", String(describing: followersGraph)), ("muteGraph", String(describing: muteGraph)), ("topHoursGraph", String(describing: topHoursGraph)), ("interactionsGraph", String(describing: interactionsGraph)), ("ivInteractionsGraph", String(describing: ivInteractionsGraph)), ("viewsBySourceGraph", String(describing: viewsBySourceGraph)), ("newFollowersBySourceGraph", String(describing: newFollowersBySourceGraph)), ("languagesGraph", String(describing: languagesGraph)), ("recentMessageInteractions", String(describing: recentMessageInteractions))]) + return ("broadcastStats", [("period", period as Any), ("followers", followers as Any), ("viewsPerPost", viewsPerPost as Any), ("sharesPerPost", sharesPerPost as Any), ("enabledNotifications", enabledNotifications as Any), ("growthGraph", growthGraph as Any), ("followersGraph", followersGraph as Any), ("muteGraph", muteGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("interactionsGraph", interactionsGraph as Any), ("ivInteractionsGraph", ivInteractionsGraph as Any), ("viewsBySourceGraph", viewsBySourceGraph as Any), ("newFollowersBySourceGraph", newFollowersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("recentMessageInteractions", recentMessageInteractions as Any)]) } } @@ -738,7 +738,7 @@ public extension Api.stats { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .megagroupStats(let period, let members, let messages, let viewers, let posters, let growthGraph, let membersGraph, let newMembersBySourceGraph, let languagesGraph, let messagesGraph, let actionsGraph, let topHoursGraph, let weekdaysGraph, let topPosters, let topAdmins, let topInviters, let users): - return ("megagroupStats", [("period", String(describing: period)), ("members", String(describing: members)), ("messages", String(describing: messages)), ("viewers", String(describing: viewers)), ("posters", String(describing: posters)), ("growthGraph", String(describing: growthGraph)), ("membersGraph", String(describing: membersGraph)), ("newMembersBySourceGraph", String(describing: newMembersBySourceGraph)), ("languagesGraph", String(describing: languagesGraph)), ("messagesGraph", String(describing: messagesGraph)), ("actionsGraph", String(describing: actionsGraph)), ("topHoursGraph", String(describing: topHoursGraph)), ("weekdaysGraph", String(describing: weekdaysGraph)), ("topPosters", String(describing: topPosters)), ("topAdmins", String(describing: topAdmins)), ("topInviters", String(describing: topInviters)), ("users", String(describing: users))]) + return ("megagroupStats", [("period", period as Any), ("members", members as Any), ("messages", messages as Any), ("viewers", viewers as Any), ("posters", posters as Any), ("growthGraph", growthGraph as Any), ("membersGraph", membersGraph as Any), ("newMembersBySourceGraph", newMembersBySourceGraph as Any), ("languagesGraph", languagesGraph as Any), ("messagesGraph", messagesGraph as Any), ("actionsGraph", actionsGraph as Any), ("topHoursGraph", topHoursGraph as Any), ("weekdaysGraph", weekdaysGraph as Any), ("topPosters", topPosters as Any), ("topAdmins", topAdmins as Any), ("topInviters", topInviters as Any), ("users", users as Any)]) } } @@ -856,7 +856,7 @@ public extension Api.stats { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .messageStats(let viewsGraph): - return ("messageStats", [("viewsGraph", String(describing: viewsGraph))]) + return ("messageStats", [("viewsGraph", viewsGraph as Any)]) } } @@ -894,7 +894,7 @@ public extension Api.stickers { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .suggestedShortName(let shortName): - return ("suggestedShortName", [("shortName", String(describing: shortName))]) + return ("suggestedShortName", [("shortName", shortName as Any)]) } } @@ -1121,11 +1121,11 @@ public extension Api.updates { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): - return ("channelDifference", [("flags", String(describing: flags)), ("pts", String(describing: pts)), ("timeout", String(describing: timeout)), ("newMessages", String(describing: newMessages)), ("otherUpdates", String(describing: otherUpdates)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("channelDifference", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any), ("newMessages", newMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any)]) case .channelDifferenceEmpty(let flags, let pts, let timeout): - return ("channelDifferenceEmpty", [("flags", String(describing: flags)), ("pts", String(describing: pts)), ("timeout", String(describing: timeout))]) + return ("channelDifferenceEmpty", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any)]) case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): - return ("channelDifferenceTooLong", [("flags", String(describing: flags)), ("timeout", String(describing: timeout)), ("dialog", String(describing: dialog)), ("messages", String(describing: messages)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("channelDifferenceTooLong", [("flags", flags as Any), ("timeout", timeout as Any), ("dialog", dialog as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1310,13 +1310,13 @@ public extension Api.updates { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .difference(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let state): - return ("difference", [("newMessages", String(describing: newMessages)), ("newEncryptedMessages", String(describing: newEncryptedMessages)), ("otherUpdates", String(describing: otherUpdates)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("state", String(describing: state))]) + return ("difference", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("state", state as Any)]) case .differenceEmpty(let date, let seq): - return ("differenceEmpty", [("date", String(describing: date)), ("seq", String(describing: seq))]) + return ("differenceEmpty", [("date", date as Any), ("seq", seq as Any)]) case .differenceSlice(let newMessages, let newEncryptedMessages, let otherUpdates, let chats, let users, let intermediateState): - return ("differenceSlice", [("newMessages", String(describing: newMessages)), ("newEncryptedMessages", String(describing: newEncryptedMessages)), ("otherUpdates", String(describing: otherUpdates)), ("chats", String(describing: chats)), ("users", String(describing: users)), ("intermediateState", String(describing: intermediateState))]) + return ("differenceSlice", [("newMessages", newMessages as Any), ("newEncryptedMessages", newEncryptedMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any), ("intermediateState", intermediateState as Any)]) case .differenceTooLong(let pts): - return ("differenceTooLong", [("pts", String(describing: pts))]) + return ("differenceTooLong", [("pts", pts as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index f82d172400d..a2670b0450f 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -20,7 +20,7 @@ public extension Api.updates { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .state(let pts, let qts, let date, let seq, let unreadCount): - return ("state", [("pts", String(describing: pts)), ("qts", String(describing: qts)), ("date", String(describing: date)), ("seq", String(describing: seq)), ("unreadCount", String(describing: unreadCount))]) + return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) } } @@ -75,9 +75,9 @@ public extension Api.upload { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .cdnFile(let bytes): - return ("cdnFile", [("bytes", String(describing: bytes))]) + return ("cdnFile", [("bytes", bytes as Any)]) case .cdnFileReuploadNeeded(let requestToken): - return ("cdnFileReuploadNeeded", [("requestToken", String(describing: requestToken))]) + return ("cdnFileReuploadNeeded", [("requestToken", requestToken as Any)]) } } @@ -141,9 +141,9 @@ public extension Api.upload { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .file(let type, let mtime, let bytes): - return ("file", [("type", String(describing: type)), ("mtime", String(describing: mtime)), ("bytes", String(describing: bytes))]) + return ("file", [("type", type as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) case .fileCdnRedirect(let dcId, let fileToken, let encryptionKey, let encryptionIv, let fileHashes): - return ("fileCdnRedirect", [("dcId", String(describing: dcId)), ("fileToken", String(describing: fileToken)), ("encryptionKey", String(describing: encryptionKey)), ("encryptionIv", String(describing: encryptionIv)), ("fileHashes", String(describing: fileHashes))]) + return ("fileCdnRedirect", [("dcId", dcId as Any), ("fileToken", fileToken as Any), ("encryptionKey", encryptionKey as Any), ("encryptionIv", encryptionIv as Any), ("fileHashes", fileHashes as Any)]) } } @@ -216,7 +216,7 @@ public extension Api.upload { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .webFile(let size, let mimeType, let fileType, let mtime, let bytes): - return ("webFile", [("size", String(describing: size)), ("mimeType", String(describing: mimeType)), ("fileType", String(describing: fileType)), ("mtime", String(describing: mtime)), ("bytes", String(describing: bytes))]) + return ("webFile", [("size", size as Any), ("mimeType", mimeType as Any), ("fileType", fileType as Any), ("mtime", mtime as Any), ("bytes", bytes as Any)]) } } @@ -276,7 +276,7 @@ public extension Api.users { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .userFull(let fullUser, let chats, let users): - return ("userFull", [("fullUser", String(describing: fullUser)), ("chats", String(describing: chats)), ("users", String(describing: users))]) + return ("userFull", [("fullUser", fullUser as Any), ("chats", chats as Any), ("users", users as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 5ca1f4e3b3e..6e87f16e186 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -2636,6 +2636,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func toggleParticipantsHidden(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1785624660) + channel.serialize(buffer, true) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "channels.toggleParticipantsHidden", parameters: [("channel", String(describing: channel)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.channels { static func togglePreHistoryHidden(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6559,12 +6575,13 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func toggleBotInAttachMenu(bot: Api.InputUser, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func toggleBotInAttachMenu(flags: Int32, bot: Api.InputUser, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(451818415) + buffer.appendInt32(1777704297) + serializeInt32(flags, buffer: buffer, boxed: false) bot.serialize(buffer, true) enabled.serialize(buffer, true) - return (FunctionDescription(name: "messages.toggleBotInAttachMenu", parameters: [("bot", String(describing: bot)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "messages.toggleBotInAttachMenu", parameters: [("flags", String(describing: flags)), ("bot", String(describing: bot)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { @@ -7548,11 +7565,31 @@ public extension Api.functions.photos { } } public extension Api.functions.photos { - static func updateProfilePhoto(id: Api.InputPhoto) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func updateProfilePhoto(flags: Int32, id: Api.InputPhoto) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1926525996) + buffer.appendInt32(473782614) + serializeInt32(flags, buffer: buffer, boxed: false) id.serialize(buffer, true) - return (FunctionDescription(name: "photos.updateProfilePhoto", parameters: [("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in + return (FunctionDescription(name: "photos.updateProfilePhoto", parameters: [("flags", String(describing: flags)), ("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in + let reader = BufferReader(buffer) + var result: Api.photos.Photo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.photos.Photo + } + return result + }) + } +} +public extension Api.functions.photos { + static func uploadContactProfilePhoto(flags: Int32, userId: Api.InputUser, file: Api.InputFile?, video: Api.InputFile?, videoStartTs: Double?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1189444673) + serializeInt32(flags, buffer: buffer, boxed: false) + userId.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {file!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {video!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {serializeDouble(videoStartTs!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "photos.uploadContactProfilePhoto", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.photos.Photo? in let reader = BufferReader(buffer) var result: Api.photos.Photo? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 0c497a877fa..6635c7318d8 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -16,7 +16,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelAdminLogEventsFilter(let flags): - return ("channelAdminLogEventsFilter", [("flags", String(describing: flags))]) + return ("channelAdminLogEventsFilter", [("flags", flags as Any)]) } } @@ -60,7 +60,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelLocation(let geoPoint, let address): - return ("channelLocation", [("geoPoint", String(describing: geoPoint)), ("address", String(describing: address))]) + return ("channelLocation", [("geoPoint", geoPoint as Any), ("address", address as Any)]) case .channelLocationEmpty: return ("channelLocationEmpty", []) } @@ -118,7 +118,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelMessagesFilter(let flags, let ranges): - return ("channelMessagesFilter", [("flags", String(describing: flags)), ("ranges", String(describing: ranges))]) + return ("channelMessagesFilter", [("flags", flags as Any), ("ranges", ranges as Any)]) case .channelMessagesFilterEmpty: return ("channelMessagesFilterEmpty", []) } @@ -216,17 +216,17 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelParticipant(let userId, let date): - return ("channelParticipant", [("userId", String(describing: userId)), ("date", String(describing: date))]) + return ("channelParticipant", [("userId", userId as Any), ("date", date as Any)]) case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank): - return ("channelParticipantAdmin", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("inviterId", String(describing: inviterId)), ("promotedBy", String(describing: promotedBy)), ("date", String(describing: date)), ("adminRights", String(describing: adminRights)), ("rank", String(describing: rank))]) + return ("channelParticipantAdmin", [("flags", flags as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("promotedBy", promotedBy as Any), ("date", date as Any), ("adminRights", adminRights as Any), ("rank", rank as Any)]) case .channelParticipantBanned(let flags, let peer, let kickedBy, let date, let bannedRights): - return ("channelParticipantBanned", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("kickedBy", String(describing: kickedBy)), ("date", String(describing: date)), ("bannedRights", String(describing: bannedRights))]) + return ("channelParticipantBanned", [("flags", flags as Any), ("peer", peer as Any), ("kickedBy", kickedBy as Any), ("date", date as Any), ("bannedRights", bannedRights as Any)]) case .channelParticipantCreator(let flags, let userId, let adminRights, let rank): - return ("channelParticipantCreator", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("adminRights", String(describing: adminRights)), ("rank", String(describing: rank))]) + return ("channelParticipantCreator", [("flags", flags as Any), ("userId", userId as Any), ("adminRights", adminRights as Any), ("rank", rank as Any)]) case .channelParticipantLeft(let peer): - return ("channelParticipantLeft", [("peer", String(describing: peer))]) + return ("channelParticipantLeft", [("peer", peer as Any)]) case .channelParticipantSelf(let flags, let userId, let inviterId, let date): - return ("channelParticipantSelf", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("inviterId", String(describing: inviterId)), ("date", String(describing: date))]) + return ("channelParticipantSelf", [("flags", flags as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any)]) } } @@ -431,19 +431,19 @@ public extension Api { case .channelParticipantsAdmins: return ("channelParticipantsAdmins", []) case .channelParticipantsBanned(let q): - return ("channelParticipantsBanned", [("q", String(describing: q))]) + return ("channelParticipantsBanned", [("q", q as Any)]) case .channelParticipantsBots: return ("channelParticipantsBots", []) case .channelParticipantsContacts(let q): - return ("channelParticipantsContacts", [("q", String(describing: q))]) + return ("channelParticipantsContacts", [("q", q as Any)]) case .channelParticipantsKicked(let q): - return ("channelParticipantsKicked", [("q", String(describing: q))]) + return ("channelParticipantsKicked", [("q", q as Any)]) case .channelParticipantsMentions(let flags, let q, let topMsgId): - return ("channelParticipantsMentions", [("flags", String(describing: flags)), ("q", String(describing: q)), ("topMsgId", String(describing: topMsgId))]) + return ("channelParticipantsMentions", [("flags", flags as Any), ("q", q as Any), ("topMsgId", topMsgId as Any)]) case .channelParticipantsRecent: return ("channelParticipantsRecent", []) case .channelParticipantsSearch(let q): - return ("channelParticipantsSearch", [("q", String(describing: q))]) + return ("channelParticipantsSearch", [("q", q as Any)]) } } @@ -601,15 +601,15 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames): - return ("channel", [("flags", String(describing: flags)), ("flags2", String(describing: flags2)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("title", String(describing: title)), ("username", String(describing: username)), ("photo", String(describing: photo)), ("date", String(describing: date)), ("restrictionReason", String(describing: restrictionReason)), ("adminRights", String(describing: adminRights)), ("bannedRights", String(describing: bannedRights)), ("defaultBannedRights", String(describing: defaultBannedRights)), ("participantsCount", String(describing: participantsCount)), ("usernames", String(describing: usernames))]) + return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any)]) case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): - return ("channelForbidden", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("title", String(describing: title)), ("untilDate", String(describing: untilDate))]) + return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)]) case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights): - return ("chat", [("flags", String(describing: flags)), ("id", String(describing: id)), ("title", String(describing: title)), ("photo", String(describing: photo)), ("participantsCount", String(describing: participantsCount)), ("date", String(describing: date)), ("version", String(describing: version)), ("migratedTo", String(describing: migratedTo)), ("adminRights", String(describing: adminRights)), ("defaultBannedRights", String(describing: defaultBannedRights))]) + return ("chat", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("date", date as Any), ("version", version as Any), ("migratedTo", migratedTo as Any), ("adminRights", adminRights as Any), ("defaultBannedRights", defaultBannedRights as Any)]) case .chatEmpty(let id): - return ("chatEmpty", [("id", String(describing: id))]) + return ("chatEmpty", [("id", id as Any)]) case .chatForbidden(let id, let title): - return ("chatForbidden", [("id", String(describing: id)), ("title", String(describing: title))]) + return ("chatForbidden", [("id", id as Any), ("title", title as Any)]) } } @@ -790,7 +790,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatAdminRights(let flags): - return ("chatAdminRights", [("flags", String(describing: flags))]) + return ("chatAdminRights", [("flags", flags as Any)]) } } @@ -828,7 +828,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatAdminWithInvites(let adminId, let invitesCount, let revokedInvitesCount): - return ("chatAdminWithInvites", [("adminId", String(describing: adminId)), ("invitesCount", String(describing: invitesCount)), ("revokedInvitesCount", String(describing: revokedInvitesCount))]) + return ("chatAdminWithInvites", [("adminId", adminId as Any), ("invitesCount", invitesCount as Any), ("revokedInvitesCount", revokedInvitesCount as Any)]) } } @@ -871,7 +871,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatBannedRights(let flags, let untilDate): - return ("chatBannedRights", [("flags", String(describing: flags)), ("untilDate", String(describing: untilDate))]) + return ("chatBannedRights", [("flags", flags as Any), ("untilDate", untilDate as Any)]) } } @@ -989,9 +989,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions): - return ("channelFull", [("flags", String(describing: flags)), ("flags2", String(describing: flags2)), ("id", String(describing: id)), ("about", String(describing: about)), ("participantsCount", String(describing: participantsCount)), ("adminsCount", String(describing: adminsCount)), ("kickedCount", String(describing: kickedCount)), ("bannedCount", String(describing: bannedCount)), ("onlineCount", String(describing: onlineCount)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("chatPhoto", String(describing: chatPhoto)), ("notifySettings", String(describing: notifySettings)), ("exportedInvite", String(describing: exportedInvite)), ("botInfo", String(describing: botInfo)), ("migratedFromChatId", String(describing: migratedFromChatId)), ("migratedFromMaxId", String(describing: migratedFromMaxId)), ("pinnedMsgId", String(describing: pinnedMsgId)), ("stickerset", String(describing: stickerset)), ("availableMinId", String(describing: availableMinId)), ("folderId", String(describing: folderId)), ("linkedChatId", String(describing: linkedChatId)), ("location", String(describing: location)), ("slowmodeSeconds", String(describing: slowmodeSeconds)), ("slowmodeNextSendDate", String(describing: slowmodeNextSendDate)), ("statsDc", String(describing: statsDc)), ("pts", String(describing: pts)), ("call", String(describing: call)), ("ttlPeriod", String(describing: ttlPeriod)), ("pendingSuggestions", String(describing: pendingSuggestions)), ("groupcallDefaultJoinAs", String(describing: groupcallDefaultJoinAs)), ("themeEmoticon", String(describing: themeEmoticon)), ("requestsPending", String(describing: requestsPending)), ("recentRequesters", String(describing: recentRequesters)), ("defaultSendAs", String(describing: defaultSendAs)), ("availableReactions", String(describing: availableReactions))]) + return ("channelFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("participantsCount", participantsCount as Any), ("adminsCount", adminsCount as Any), ("kickedCount", kickedCount as Any), ("bannedCount", bannedCount as Any), ("onlineCount", onlineCount as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("chatPhoto", chatPhoto as Any), ("notifySettings", notifySettings as Any), ("exportedInvite", exportedInvite as Any), ("botInfo", botInfo as Any), ("migratedFromChatId", migratedFromChatId as Any), ("migratedFromMaxId", migratedFromMaxId as Any), ("pinnedMsgId", pinnedMsgId as Any), ("stickerset", stickerset as Any), ("availableMinId", availableMinId as Any), ("folderId", folderId as Any), ("linkedChatId", linkedChatId as Any), ("location", location as Any), ("slowmodeSeconds", slowmodeSeconds as Any), ("slowmodeNextSendDate", slowmodeNextSendDate as Any), ("statsDc", statsDc as Any), ("pts", pts as Any), ("call", call as Any), ("ttlPeriod", ttlPeriod as Any), ("pendingSuggestions", pendingSuggestions as Any), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs as Any), ("themeEmoticon", themeEmoticon as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any), ("defaultSendAs", defaultSendAs as Any), ("availableReactions", availableReactions as Any)]) case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): - return ("chatFull", [("flags", String(describing: flags)), ("id", String(describing: id)), ("about", String(describing: about)), ("participants", String(describing: participants)), ("chatPhoto", String(describing: chatPhoto)), ("notifySettings", String(describing: notifySettings)), ("exportedInvite", String(describing: exportedInvite)), ("botInfo", String(describing: botInfo)), ("pinnedMsgId", String(describing: pinnedMsgId)), ("folderId", String(describing: folderId)), ("call", String(describing: call)), ("ttlPeriod", String(describing: ttlPeriod)), ("groupcallDefaultJoinAs", String(describing: groupcallDefaultJoinAs)), ("themeEmoticon", String(describing: themeEmoticon)), ("requestsPending", String(describing: requestsPending)), ("recentRequesters", String(describing: recentRequesters)), ("availableReactions", String(describing: availableReactions))]) + return ("chatFull", [("flags", flags as Any), ("id", id as Any), ("about", about as Any), ("participants", participants as Any), ("chatPhoto", chatPhoto as Any), ("notifySettings", notifySettings as Any), ("exportedInvite", exportedInvite as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("folderId", folderId as Any), ("call", call as Any), ("ttlPeriod", ttlPeriod as Any), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs as Any), ("themeEmoticon", themeEmoticon as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any), ("availableReactions", availableReactions as Any)]) } } @@ -1260,11 +1260,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants): - return ("chatInvite", [("flags", String(describing: flags)), ("title", String(describing: title)), ("about", String(describing: about)), ("photo", String(describing: photo)), ("participantsCount", String(describing: participantsCount)), ("participants", String(describing: participants))]) + return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any)]) case .chatInviteAlready(let chat): - return ("chatInviteAlready", [("chat", String(describing: chat))]) + return ("chatInviteAlready", [("chat", chat as Any)]) case .chatInvitePeek(let chat, let expires): - return ("chatInvitePeek", [("chat", String(describing: chat)), ("expires", String(describing: expires))]) + return ("chatInvitePeek", [("chat", chat as Any), ("expires", expires as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 9b7ed73b7a8..6d40a03871c 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -20,7 +20,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatInviteImporter(let flags, let userId, let date, let about, let approvedBy): - return ("chatInviteImporter", [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("date", String(describing: date)), ("about", String(describing: about)), ("approvedBy", String(describing: approvedBy))]) + return ("chatInviteImporter", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("about", about as Any), ("approvedBy", approvedBy as Any)]) } } @@ -68,7 +68,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatOnlines(let onlines): - return ("chatOnlines", [("onlines", String(describing: onlines))]) + return ("chatOnlines", [("onlines", onlines as Any)]) } } @@ -122,11 +122,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatParticipant(let userId, let inviterId, let date): - return ("chatParticipant", [("userId", String(describing: userId)), ("inviterId", String(describing: inviterId)), ("date", String(describing: date))]) + return ("chatParticipant", [("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any)]) case .chatParticipantAdmin(let userId, let inviterId, let date): - return ("chatParticipantAdmin", [("userId", String(describing: userId)), ("inviterId", String(describing: inviterId)), ("date", String(describing: date))]) + return ("chatParticipantAdmin", [("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any)]) case .chatParticipantCreator(let userId): - return ("chatParticipantCreator", [("userId", String(describing: userId))]) + return ("chatParticipantCreator", [("userId", userId as Any)]) } } @@ -211,9 +211,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatParticipants(let chatId, let participants, let version): - return ("chatParticipants", [("chatId", String(describing: chatId)), ("participants", String(describing: participants)), ("version", String(describing: version))]) + return ("chatParticipants", [("chatId", chatId as Any), ("participants", participants as Any), ("version", version as Any)]) case .chatParticipantsForbidden(let flags, let chatId, let selfParticipant): - return ("chatParticipantsForbidden", [("flags", String(describing: flags)), ("chatId", String(describing: chatId)), ("selfParticipant", String(describing: selfParticipant))]) + return ("chatParticipantsForbidden", [("flags", flags as Any), ("chatId", chatId as Any), ("selfParticipant", selfParticipant as Any)]) } } @@ -286,7 +286,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatPhoto(let flags, let photoId, let strippedThumb, let dcId): - return ("chatPhoto", [("flags", String(describing: flags)), ("photoId", String(describing: photoId)), ("strippedThumb", String(describing: strippedThumb)), ("dcId", String(describing: dcId))]) + return ("chatPhoto", [("flags", flags as Any), ("photoId", photoId as Any), ("strippedThumb", strippedThumb as Any), ("dcId", dcId as Any)]) case .chatPhotoEmpty: return ("chatPhotoEmpty", []) } @@ -354,11 +354,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatReactionsAll(let flags): - return ("chatReactionsAll", [("flags", String(describing: flags))]) + return ("chatReactionsAll", [("flags", flags as Any)]) case .chatReactionsNone: return ("chatReactionsNone", []) case .chatReactionsSome(let reactions): - return ("chatReactionsSome", [("reactions", String(describing: reactions))]) + return ("chatReactionsSome", [("reactions", reactions as Any)]) } } @@ -415,7 +415,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .codeSettings(let flags, let logoutTokens): - return ("codeSettings", [("flags", String(describing: flags)), ("logoutTokens", String(describing: logoutTokens))]) + return ("codeSettings", [("flags", flags as Any), ("logoutTokens", logoutTokens as Any)]) } } @@ -505,7 +505,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .config(let flags, let date, let expires, let testMode, let thisDc, let dcOptions, let dcTxtDomainName, let chatSizeMax, let megagroupSizeMax, let forwardedCountMax, let onlineUpdatePeriodMs, let offlineBlurTimeoutMs, let offlineIdleTimeoutMs, let onlineCloudTimeoutMs, let notifyCloudDelayMs, let notifyDefaultDelayMs, let pushChatPeriodMs, let pushChatLimit, let savedGifsLimit, let editTimeLimit, let revokeTimeLimit, let revokePmTimeLimit, let ratingEDecay, let stickersRecentLimit, let stickersFavedLimit, let channelsReadMediaPeriod, let tmpSessions, let pinnedDialogsCountMax, let pinnedInfolderCountMax, let callReceiveTimeoutMs, let callRingTimeoutMs, let callConnectTimeoutMs, let callPacketTimeoutMs, let meUrlPrefix, let autoupdateUrlPrefix, let gifSearchUsername, let venueSearchUsername, let imgSearchUsername, let staticMapsProvider, let captionLengthMax, let messageLengthMax, let webfileDcId, let suggestedLangCode, let langPackVersion, let baseLangPackVersion, let reactionsDefault): - return ("config", [("flags", String(describing: flags)), ("date", String(describing: date)), ("expires", String(describing: expires)), ("testMode", String(describing: testMode)), ("thisDc", String(describing: thisDc)), ("dcOptions", String(describing: dcOptions)), ("dcTxtDomainName", String(describing: dcTxtDomainName)), ("chatSizeMax", String(describing: chatSizeMax)), ("megagroupSizeMax", String(describing: megagroupSizeMax)), ("forwardedCountMax", String(describing: forwardedCountMax)), ("onlineUpdatePeriodMs", String(describing: onlineUpdatePeriodMs)), ("offlineBlurTimeoutMs", String(describing: offlineBlurTimeoutMs)), ("offlineIdleTimeoutMs", String(describing: offlineIdleTimeoutMs)), ("onlineCloudTimeoutMs", String(describing: onlineCloudTimeoutMs)), ("notifyCloudDelayMs", String(describing: notifyCloudDelayMs)), ("notifyDefaultDelayMs", String(describing: notifyDefaultDelayMs)), ("pushChatPeriodMs", String(describing: pushChatPeriodMs)), ("pushChatLimit", String(describing: pushChatLimit)), ("savedGifsLimit", String(describing: savedGifsLimit)), ("editTimeLimit", String(describing: editTimeLimit)), ("revokeTimeLimit", String(describing: revokeTimeLimit)), ("revokePmTimeLimit", String(describing: revokePmTimeLimit)), ("ratingEDecay", String(describing: ratingEDecay)), ("stickersRecentLimit", String(describing: stickersRecentLimit)), ("stickersFavedLimit", String(describing: stickersFavedLimit)), ("channelsReadMediaPeriod", String(describing: channelsReadMediaPeriod)), ("tmpSessions", String(describing: tmpSessions)), ("pinnedDialogsCountMax", String(describing: pinnedDialogsCountMax)), ("pinnedInfolderCountMax", String(describing: pinnedInfolderCountMax)), ("callReceiveTimeoutMs", String(describing: callReceiveTimeoutMs)), ("callRingTimeoutMs", String(describing: callRingTimeoutMs)), ("callConnectTimeoutMs", String(describing: callConnectTimeoutMs)), ("callPacketTimeoutMs", String(describing: callPacketTimeoutMs)), ("meUrlPrefix", String(describing: meUrlPrefix)), ("autoupdateUrlPrefix", String(describing: autoupdateUrlPrefix)), ("gifSearchUsername", String(describing: gifSearchUsername)), ("venueSearchUsername", String(describing: venueSearchUsername)), ("imgSearchUsername", String(describing: imgSearchUsername)), ("staticMapsProvider", String(describing: staticMapsProvider)), ("captionLengthMax", String(describing: captionLengthMax)), ("messageLengthMax", String(describing: messageLengthMax)), ("webfileDcId", String(describing: webfileDcId)), ("suggestedLangCode", String(describing: suggestedLangCode)), ("langPackVersion", String(describing: langPackVersion)), ("baseLangPackVersion", String(describing: baseLangPackVersion)), ("reactionsDefault", String(describing: reactionsDefault))]) + return ("config", [("flags", flags as Any), ("date", date as Any), ("expires", expires as Any), ("testMode", testMode as Any), ("thisDc", thisDc as Any), ("dcOptions", dcOptions as Any), ("dcTxtDomainName", dcTxtDomainName as Any), ("chatSizeMax", chatSizeMax as Any), ("megagroupSizeMax", megagroupSizeMax as Any), ("forwardedCountMax", forwardedCountMax as Any), ("onlineUpdatePeriodMs", onlineUpdatePeriodMs as Any), ("offlineBlurTimeoutMs", offlineBlurTimeoutMs as Any), ("offlineIdleTimeoutMs", offlineIdleTimeoutMs as Any), ("onlineCloudTimeoutMs", onlineCloudTimeoutMs as Any), ("notifyCloudDelayMs", notifyCloudDelayMs as Any), ("notifyDefaultDelayMs", notifyDefaultDelayMs as Any), ("pushChatPeriodMs", pushChatPeriodMs as Any), ("pushChatLimit", pushChatLimit as Any), ("savedGifsLimit", savedGifsLimit as Any), ("editTimeLimit", editTimeLimit as Any), ("revokeTimeLimit", revokeTimeLimit as Any), ("revokePmTimeLimit", revokePmTimeLimit as Any), ("ratingEDecay", ratingEDecay as Any), ("stickersRecentLimit", stickersRecentLimit as Any), ("stickersFavedLimit", stickersFavedLimit as Any), ("channelsReadMediaPeriod", channelsReadMediaPeriod as Any), ("tmpSessions", tmpSessions as Any), ("pinnedDialogsCountMax", pinnedDialogsCountMax as Any), ("pinnedInfolderCountMax", pinnedInfolderCountMax as Any), ("callReceiveTimeoutMs", callReceiveTimeoutMs as Any), ("callRingTimeoutMs", callRingTimeoutMs as Any), ("callConnectTimeoutMs", callConnectTimeoutMs as Any), ("callPacketTimeoutMs", callPacketTimeoutMs as Any), ("meUrlPrefix", meUrlPrefix as Any), ("autoupdateUrlPrefix", autoupdateUrlPrefix as Any), ("gifSearchUsername", gifSearchUsername as Any), ("venueSearchUsername", venueSearchUsername as Any), ("imgSearchUsername", imgSearchUsername as Any), ("staticMapsProvider", staticMapsProvider as Any), ("captionLengthMax", captionLengthMax as Any), ("messageLengthMax", messageLengthMax as Any), ("webfileDcId", webfileDcId as Any), ("suggestedLangCode", suggestedLangCode as Any), ("langPackVersion", langPackVersion as Any), ("baseLangPackVersion", baseLangPackVersion as Any), ("reactionsDefault", reactionsDefault as Any)]) } } @@ -683,7 +683,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .contact(let userId, let mutual): - return ("contact", [("userId", String(describing: userId)), ("mutual", String(describing: mutual))]) + return ("contact", [("userId", userId as Any), ("mutual", mutual as Any)]) } } @@ -725,7 +725,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .contactStatus(let userId, let status): - return ("contactStatus", [("userId", String(describing: userId)), ("status", String(describing: status))]) + return ("contactStatus", [("userId", userId as Any), ("status", status as Any)]) } } @@ -766,7 +766,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dataJSON(let data): - return ("dataJSON", [("data", String(describing: data))]) + return ("dataJSON", [("data", data as Any)]) } } @@ -806,7 +806,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dcOption(let flags, let id, let ipAddress, let port, let secret): - return ("dcOption", [("flags", String(describing: flags)), ("id", String(describing: id)), ("ipAddress", String(describing: ipAddress)), ("port", String(describing: port)), ("secret", String(describing: secret))]) + return ("dcOption", [("flags", flags as Any), ("id", id as Any), ("ipAddress", ipAddress as Any), ("port", port as Any), ("secret", secret as Any)]) } } @@ -854,7 +854,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .defaultHistoryTTL(let period): - return ("defaultHistoryTTL", [("period", String(describing: period))]) + return ("defaultHistoryTTL", [("period", period as Any)]) } } @@ -916,9 +916,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dialog(let flags, let peer, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount, let notifySettings, let pts, let draft, let folderId, let ttlPeriod): - return ("dialog", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("topMessage", String(describing: topMessage)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("unreadMentionsCount", String(describing: unreadMentionsCount)), ("unreadReactionsCount", String(describing: unreadReactionsCount)), ("notifySettings", String(describing: notifySettings)), ("pts", String(describing: pts)), ("draft", String(describing: draft)), ("folderId", String(describing: folderId)), ("ttlPeriod", String(describing: ttlPeriod))]) + return ("dialog", [("flags", flags as Any), ("peer", peer as Any), ("topMessage", topMessage as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("unreadMentionsCount", unreadMentionsCount as Any), ("unreadReactionsCount", unreadReactionsCount as Any), ("notifySettings", notifySettings as Any), ("pts", pts as Any), ("draft", draft as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any)]) case .dialogFolder(let flags, let folder, let peer, let topMessage, let unreadMutedPeersCount, let unreadUnmutedPeersCount, let unreadMutedMessagesCount, let unreadUnmutedMessagesCount): - return ("dialogFolder", [("flags", String(describing: flags)), ("folder", String(describing: folder)), ("peer", String(describing: peer)), ("topMessage", String(describing: topMessage)), ("unreadMutedPeersCount", String(describing: unreadMutedPeersCount)), ("unreadUnmutedPeersCount", String(describing: unreadUnmutedPeersCount)), ("unreadMutedMessagesCount", String(describing: unreadMutedMessagesCount)), ("unreadUnmutedMessagesCount", String(describing: unreadUnmutedMessagesCount))]) + return ("dialogFolder", [("flags", flags as Any), ("folder", folder as Any), ("peer", peer as Any), ("topMessage", topMessage as Any), ("unreadMutedPeersCount", unreadMutedPeersCount as Any), ("unreadUnmutedPeersCount", unreadUnmutedPeersCount as Any), ("unreadMutedMessagesCount", unreadMutedMessagesCount as Any), ("unreadUnmutedMessagesCount", unreadUnmutedMessagesCount as Any)]) } } @@ -1057,7 +1057,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dialogFilter(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers, let excludePeers): - return ("dialogFilter", [("flags", String(describing: flags)), ("id", String(describing: id)), ("title", String(describing: title)), ("emoticon", String(describing: emoticon)), ("pinnedPeers", String(describing: pinnedPeers)), ("includePeers", String(describing: includePeers)), ("excludePeers", String(describing: excludePeers))]) + return ("dialogFilter", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("emoticon", emoticon as Any), ("pinnedPeers", pinnedPeers as Any), ("includePeers", includePeers as Any), ("excludePeers", excludePeers as Any)]) case .dialogFilterDefault: return ("dialogFilterDefault", []) } @@ -1123,7 +1123,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dialogFilterSuggested(let filter, let description): - return ("dialogFilterSuggested", [("filter", String(describing: filter)), ("description", String(describing: description))]) + return ("dialogFilterSuggested", [("filter", filter as Any), ("description", description as Any)]) } } @@ -1171,9 +1171,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .dialogPeer(let peer): - return ("dialogPeer", [("peer", String(describing: peer))]) + return ("dialogPeer", [("peer", peer as Any)]) case .dialogPeerFolder(let folderId): - return ("dialogPeerFolder", [("folderId", String(describing: folderId))]) + return ("dialogPeerFolder", [("folderId", folderId as Any)]) } } @@ -1251,9 +1251,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .document(let flags, let id, let accessHash, let fileReference, let date, let mimeType, let size, let thumbs, let videoThumbs, let dcId, let attributes): - return ("document", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("mimeType", String(describing: mimeType)), ("size", String(describing: size)), ("thumbs", String(describing: thumbs)), ("videoThumbs", String(describing: videoThumbs)), ("dcId", String(describing: dcId)), ("attributes", String(describing: attributes))]) + return ("document", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("date", date as Any), ("mimeType", mimeType as Any), ("size", size as Any), ("thumbs", thumbs as Any), ("videoThumbs", videoThumbs as Any), ("dcId", dcId as Any), ("attributes", attributes as Any)]) case .documentEmpty(let id): - return ("documentEmpty", [("id", String(describing: id))]) + return ("documentEmpty", [("id", id as Any)]) } } @@ -1400,19 +1400,19 @@ public extension Api { case .documentAttributeAnimated: return ("documentAttributeAnimated", []) case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): - return ("documentAttributeAudio", [("flags", String(describing: flags)), ("duration", String(describing: duration)), ("title", String(describing: title)), ("performer", String(describing: performer)), ("waveform", String(describing: waveform))]) + return ("documentAttributeAudio", [("flags", flags as Any), ("duration", duration as Any), ("title", title as Any), ("performer", performer as Any), ("waveform", waveform as Any)]) case .documentAttributeCustomEmoji(let flags, let alt, let stickerset): - return ("documentAttributeCustomEmoji", [("flags", String(describing: flags)), ("alt", String(describing: alt)), ("stickerset", String(describing: stickerset))]) + return ("documentAttributeCustomEmoji", [("flags", flags as Any), ("alt", alt as Any), ("stickerset", stickerset as Any)]) case .documentAttributeFilename(let fileName): - return ("documentAttributeFilename", [("fileName", String(describing: fileName))]) + return ("documentAttributeFilename", [("fileName", fileName as Any)]) case .documentAttributeHasStickers: return ("documentAttributeHasStickers", []) case .documentAttributeImageSize(let w, let h): - return ("documentAttributeImageSize", [("w", String(describing: w)), ("h", String(describing: h))]) + return ("documentAttributeImageSize", [("w", w as Any), ("h", h as Any)]) case .documentAttributeSticker(let flags, let alt, let stickerset, let maskCoords): - return ("documentAttributeSticker", [("flags", String(describing: flags)), ("alt", String(describing: alt)), ("stickerset", String(describing: stickerset)), ("maskCoords", String(describing: maskCoords))]) + return ("documentAttributeSticker", [("flags", flags as Any), ("alt", alt as Any), ("stickerset", stickerset as Any), ("maskCoords", maskCoords as Any)]) case .documentAttributeVideo(let flags, let duration, let w, let h): - return ("documentAttributeVideo", [("flags", String(describing: flags)), ("duration", String(describing: duration)), ("w", String(describing: w)), ("h", String(describing: h))]) + return ("documentAttributeVideo", [("flags", flags as Any), ("duration", duration as Any), ("w", w as Any), ("h", h as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index 42ddc4ef5a4..43a2b20dc3d 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -32,9 +32,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .draftMessage(let flags, let replyToMsgId, let message, let entities, let date): - return ("draftMessage", [("flags", String(describing: flags)), ("replyToMsgId", String(describing: replyToMsgId)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("date", String(describing: date))]) + return ("draftMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("message", message as Any), ("entities", entities as Any), ("date", date as Any)]) case .draftMessageEmpty(let flags, let date): - return ("draftMessageEmpty", [("flags", String(describing: flags)), ("date", String(describing: date))]) + return ("draftMessageEmpty", [("flags", flags as Any), ("date", date as Any)]) } } @@ -112,11 +112,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emailVerificationApple(let token): - return ("emailVerificationApple", [("token", String(describing: token))]) + return ("emailVerificationApple", [("token", token as Any)]) case .emailVerificationCode(let code): - return ("emailVerificationCode", [("code", String(describing: code))]) + return ("emailVerificationCode", [("code", code as Any)]) case .emailVerificationGoogle(let token): - return ("emailVerificationGoogle", [("token", String(describing: token))]) + return ("emailVerificationGoogle", [("token", token as Any)]) } } @@ -191,7 +191,7 @@ public extension Api { case .emailVerifyPurposeLoginChange: return ("emailVerifyPurposeLoginChange", []) case .emailVerifyPurposeLoginSetup(let phoneNumber, let phoneCodeHash): - return ("emailVerifyPurposeLoginSetup", [("phoneNumber", String(describing: phoneNumber)), ("phoneCodeHash", String(describing: phoneCodeHash))]) + return ("emailVerifyPurposeLoginSetup", [("phoneNumber", phoneNumber as Any), ("phoneCodeHash", phoneCodeHash as Any)]) case .emailVerifyPurposePassport: return ("emailVerifyPurposePassport", []) } @@ -255,9 +255,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiKeyword(let keyword, let emoticons): - return ("emojiKeyword", [("keyword", String(describing: keyword)), ("emoticons", String(describing: emoticons))]) + return ("emojiKeyword", [("keyword", keyword as Any), ("emoticons", emoticons as Any)]) case .emojiKeywordDeleted(let keyword, let emoticons): - return ("emojiKeywordDeleted", [("keyword", String(describing: keyword)), ("emoticons", String(describing: emoticons))]) + return ("emojiKeywordDeleted", [("keyword", keyword as Any), ("emoticons", emoticons as Any)]) } } @@ -321,7 +321,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiKeywordsDifference(let langCode, let fromVersion, let version, let keywords): - return ("emojiKeywordsDifference", [("langCode", String(describing: langCode)), ("fromVersion", String(describing: fromVersion)), ("version", String(describing: version)), ("keywords", String(describing: keywords))]) + return ("emojiKeywordsDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("keywords", keywords as Any)]) } } @@ -368,7 +368,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiLanguage(let langCode): - return ("emojiLanguage", [("langCode", String(describing: langCode))]) + return ("emojiLanguage", [("langCode", langCode as Any)]) } } @@ -419,11 +419,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiStatus(let documentId): - return ("emojiStatus", [("documentId", String(describing: documentId))]) + return ("emojiStatus", [("documentId", documentId as Any)]) case .emojiStatusEmpty: return ("emojiStatusEmpty", []) case .emojiStatusUntil(let documentId, let until): - return ("emojiStatusUntil", [("documentId", String(describing: documentId)), ("until", String(describing: until))]) + return ("emojiStatusUntil", [("documentId", documentId as Any), ("until", until as Any)]) } } @@ -476,7 +476,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .emojiURL(let url): - return ("emojiURL", [("url", String(describing: url))]) + return ("emojiURL", [("url", url as Any)]) } } @@ -558,15 +558,15 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .encryptedChat(let id, let accessHash, let date, let adminId, let participantId, let gAOrB, let keyFingerprint): - return ("encryptedChat", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("gAOrB", String(describing: gAOrB)), ("keyFingerprint", String(describing: keyFingerprint))]) + return ("encryptedChat", [("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("gAOrB", gAOrB as Any), ("keyFingerprint", keyFingerprint as Any)]) case .encryptedChatDiscarded(let flags, let id): - return ("encryptedChatDiscarded", [("flags", String(describing: flags)), ("id", String(describing: id))]) + return ("encryptedChatDiscarded", [("flags", flags as Any), ("id", id as Any)]) case .encryptedChatEmpty(let id): - return ("encryptedChatEmpty", [("id", String(describing: id))]) + return ("encryptedChatEmpty", [("id", id as Any)]) case .encryptedChatRequested(let flags, let folderId, let id, let accessHash, let date, let adminId, let participantId, let gA): - return ("encryptedChatRequested", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId)), ("gA", String(describing: gA))]) + return ("encryptedChatRequested", [("flags", flags as Any), ("folderId", folderId as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any), ("gA", gA as Any)]) case .encryptedChatWaiting(let id, let accessHash, let date, let adminId, let participantId): - return ("encryptedChatWaiting", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("date", String(describing: date)), ("adminId", String(describing: adminId)), ("participantId", String(describing: participantId))]) + return ("encryptedChatWaiting", [("id", id as Any), ("accessHash", accessHash as Any), ("date", date as Any), ("adminId", adminId as Any), ("participantId", participantId as Any)]) } } @@ -711,7 +711,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .encryptedFile(let id, let accessHash, let size, let dcId, let keyFingerprint): - return ("encryptedFile", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("size", String(describing: size)), ("dcId", String(describing: dcId)), ("keyFingerprint", String(describing: keyFingerprint))]) + return ("encryptedFile", [("id", id as Any), ("accessHash", accessHash as Any), ("size", size as Any), ("dcId", dcId as Any), ("keyFingerprint", keyFingerprint as Any)]) case .encryptedFileEmpty: return ("encryptedFileEmpty", []) } @@ -778,9 +778,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .encryptedMessage(let randomId, let chatId, let date, let bytes, let file): - return ("encryptedMessage", [("randomId", String(describing: randomId)), ("chatId", String(describing: chatId)), ("date", String(describing: date)), ("bytes", String(describing: bytes)), ("file", String(describing: file))]) + return ("encryptedMessage", [("randomId", randomId as Any), ("chatId", chatId as Any), ("date", date as Any), ("bytes", bytes as Any), ("file", file as Any)]) case .encryptedMessageService(let randomId, let chatId, let date, let bytes): - return ("encryptedMessageService", [("randomId", String(describing: randomId)), ("chatId", String(describing: chatId)), ("date", String(describing: date)), ("bytes", String(describing: bytes))]) + return ("encryptedMessageService", [("randomId", randomId as Any), ("chatId", chatId as Any), ("date", date as Any), ("bytes", bytes as Any)]) } } @@ -866,7 +866,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title): - return ("chatInviteExported", [("flags", String(describing: flags)), ("link", String(describing: link)), ("adminId", String(describing: adminId)), ("date", String(describing: date)), ("startDate", String(describing: startDate)), ("expireDate", String(describing: expireDate)), ("usageLimit", String(describing: usageLimit)), ("usage", String(describing: usage)), ("requested", String(describing: requested)), ("title", String(describing: title))]) + return ("chatInviteExported", [("flags", flags as Any), ("link", link as Any), ("adminId", adminId as Any), ("date", date as Any), ("startDate", startDate as Any), ("expireDate", expireDate as Any), ("usageLimit", usageLimit as Any), ("usage", usage as Any), ("requested", requested as Any), ("title", title as Any)]) case .chatInvitePublicJoinRequests: return ("chatInvitePublicJoinRequests", []) } @@ -935,7 +935,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedContactToken(let url, let expires): - return ("exportedContactToken", [("url", String(describing: url)), ("expires", String(describing: expires))]) + return ("exportedContactToken", [("url", url as Any), ("expires", expires as Any)]) } } @@ -975,7 +975,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .exportedMessageLink(let link, let html): - return ("exportedMessageLink", [("link", String(describing: link)), ("html", String(describing: html))]) + return ("exportedMessageLink", [("link", link as Any), ("html", html as Any)]) } } @@ -1016,7 +1016,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .fileHash(let offset, let limit, let hash): - return ("fileHash", [("offset", String(describing: offset)), ("limit", String(describing: limit)), ("hash", String(describing: hash))]) + return ("fileHash", [("offset", offset as Any), ("limit", limit as Any), ("hash", hash as Any)]) } } @@ -1061,7 +1061,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .folder(let flags, let id, let title, let photo): - return ("folder", [("flags", String(describing: flags)), ("id", String(describing: id)), ("title", String(describing: title)), ("photo", String(describing: photo))]) + return ("folder", [("flags", flags as Any), ("id", id as Any), ("title", title as Any), ("photo", photo as Any)]) } } @@ -1109,7 +1109,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .folderPeer(let peer, let folderId): - return ("folderPeer", [("peer", String(describing: peer)), ("folderId", String(describing: folderId))]) + return ("folderPeer", [("peer", peer as Any), ("folderId", folderId as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index f59d75fea7b..0423e82e22a 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -37,9 +37,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .forumTopic(let flags, let id, let date, let title, let iconColor, let iconEmojiId, let topMessage, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let unreadMentionsCount, let unreadReactionsCount, let fromId, let notifySettings, let draft): - return ("forumTopic", [("flags", String(describing: flags)), ("id", String(describing: id)), ("date", String(describing: date)), ("title", String(describing: title)), ("iconColor", String(describing: iconColor)), ("iconEmojiId", String(describing: iconEmojiId)), ("topMessage", String(describing: topMessage)), ("readInboxMaxId", String(describing: readInboxMaxId)), ("readOutboxMaxId", String(describing: readOutboxMaxId)), ("unreadCount", String(describing: unreadCount)), ("unreadMentionsCount", String(describing: unreadMentionsCount)), ("unreadReactionsCount", String(describing: unreadReactionsCount)), ("fromId", String(describing: fromId)), ("notifySettings", String(describing: notifySettings)), ("draft", String(describing: draft))]) + return ("forumTopic", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("title", title as Any), ("iconColor", iconColor as Any), ("iconEmojiId", iconEmojiId as Any), ("topMessage", topMessage as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("unreadMentionsCount", unreadMentionsCount as Any), ("unreadReactionsCount", unreadReactionsCount as Any), ("fromId", fromId as Any), ("notifySettings", notifySettings as Any), ("draft", draft as Any)]) case .forumTopicDeleted(let id): - return ("forumTopicDeleted", [("id", String(describing: id))]) + return ("forumTopicDeleted", [("id", id as Any)]) } } @@ -141,7 +141,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .game(let flags, let id, let accessHash, let shortName, let title, let description, let photo, let document): - return ("game", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("shortName", String(describing: shortName)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("document", String(describing: document))]) + return ("game", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("shortName", shortName as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("document", document as Any)]) } } @@ -213,7 +213,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .geoPoint(let flags, let long, let lat, let accessHash, let accuracyRadius): - return ("geoPoint", [("flags", String(describing: flags)), ("long", String(describing: long)), ("lat", String(describing: lat)), ("accessHash", String(describing: accessHash)), ("accuracyRadius", String(describing: accuracyRadius))]) + return ("geoPoint", [("flags", flags as Any), ("long", long as Any), ("lat", lat as Any), ("accessHash", accessHash as Any), ("accuracyRadius", accuracyRadius as Any)]) case .geoPointEmpty: return ("geoPointEmpty", []) } @@ -267,7 +267,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .globalPrivacySettings(let flags, let archiveAndMuteNewNoncontactPeers): - return ("globalPrivacySettings", [("flags", String(describing: flags)), ("archiveAndMuteNewNoncontactPeers", String(describing: archiveAndMuteNewNoncontactPeers))]) + return ("globalPrivacySettings", [("flags", flags as Any), ("archiveAndMuteNewNoncontactPeers", archiveAndMuteNewNoncontactPeers as Any)]) } } @@ -327,9 +327,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCall(let flags, let id, let accessHash, let participantsCount, let title, let streamDcId, let recordStartDate, let scheduleDate, let unmutedVideoCount, let unmutedVideoLimit, let version): - return ("groupCall", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("participantsCount", String(describing: participantsCount)), ("title", String(describing: title)), ("streamDcId", String(describing: streamDcId)), ("recordStartDate", String(describing: recordStartDate)), ("scheduleDate", String(describing: scheduleDate)), ("unmutedVideoCount", String(describing: unmutedVideoCount)), ("unmutedVideoLimit", String(describing: unmutedVideoLimit)), ("version", String(describing: version))]) + return ("groupCall", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("participantsCount", participantsCount as Any), ("title", title as Any), ("streamDcId", streamDcId as Any), ("recordStartDate", recordStartDate as Any), ("scheduleDate", scheduleDate as Any), ("unmutedVideoCount", unmutedVideoCount as Any), ("unmutedVideoLimit", unmutedVideoLimit as Any), ("version", version as Any)]) case .groupCallDiscarded(let id, let accessHash, let duration): - return ("groupCallDiscarded", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("duration", String(describing: duration))]) + return ("groupCallDiscarded", [("id", id as Any), ("accessHash", accessHash as Any), ("duration", duration as Any)]) } } @@ -421,7 +421,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallParticipant(let flags, let peer, let date, let activeDate, let source, let volume, let about, let raiseHandRating, let video, let presentation): - return ("groupCallParticipant", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("date", String(describing: date)), ("activeDate", String(describing: activeDate)), ("source", String(describing: source)), ("volume", String(describing: volume)), ("about", String(describing: about)), ("raiseHandRating", String(describing: raiseHandRating)), ("video", String(describing: video)), ("presentation", String(describing: presentation))]) + return ("groupCallParticipant", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("activeDate", activeDate as Any), ("source", source as Any), ("volume", volume as Any), ("about", about as Any), ("raiseHandRating", raiseHandRating as Any), ("video", video as Any), ("presentation", presentation as Any)]) } } @@ -497,7 +497,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallParticipantVideo(let flags, let endpoint, let sourceGroups, let audioSource): - return ("groupCallParticipantVideo", [("flags", String(describing: flags)), ("endpoint", String(describing: endpoint)), ("sourceGroups", String(describing: sourceGroups)), ("audioSource", String(describing: audioSource))]) + return ("groupCallParticipantVideo", [("flags", flags as Any), ("endpoint", endpoint as Any), ("sourceGroups", sourceGroups as Any), ("audioSource", audioSource as Any)]) } } @@ -549,7 +549,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallParticipantVideoSourceGroup(let semantics, let sources): - return ("groupCallParticipantVideoSourceGroup", [("semantics", String(describing: semantics)), ("sources", String(describing: sources))]) + return ("groupCallParticipantVideoSourceGroup", [("semantics", semantics as Any), ("sources", sources as Any)]) } } @@ -592,7 +592,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .groupCallStreamChannel(let channel, let scale, let lastTimestampMs): - return ("groupCallStreamChannel", [("channel", String(describing: channel)), ("scale", String(describing: scale)), ("lastTimestampMs", String(describing: lastTimestampMs))]) + return ("groupCallStreamChannel", [("channel", channel as Any), ("scale", scale as Any), ("lastTimestampMs", lastTimestampMs as Any)]) } } @@ -636,7 +636,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .highScore(let pos, let userId, let score): - return ("highScore", [("pos", String(describing: pos)), ("userId", String(describing: userId)), ("score", String(describing: score))]) + return ("highScore", [("pos", pos as Any), ("userId", userId as Any), ("score", score as Any)]) } } @@ -679,7 +679,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .importedContact(let userId, let clientId): - return ("importedContact", [("userId", String(describing: userId)), ("clientId", String(describing: clientId))]) + return ("importedContact", [("userId", userId as Any), ("clientId", clientId as Any)]) } } @@ -719,7 +719,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inlineBotSwitchPM(let text, let startParam): - return ("inlineBotSwitchPM", [("text", String(describing: text)), ("startParam", String(describing: startParam))]) + return ("inlineBotSwitchPM", [("text", text as Any), ("startParam", startParam as Any)]) } } @@ -837,7 +837,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputAppEvent(let time, let type, let peer, let data): - return ("inputAppEvent", [("time", String(describing: time)), ("type", String(describing: type)), ("peer", String(describing: peer)), ("data", String(describing: data))]) + return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) } } @@ -966,19 +966,19 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputBotInlineMessageGame(let flags, let replyMarkup): - return ("inputBotInlineMessageGame", [("flags", String(describing: flags)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageGame", [("flags", flags as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): - return ("inputBotInlineMessageMediaAuto", [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageMediaAuto", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): - return ("inputBotInlineMessageMediaContact", [("flags", String(describing: flags)), ("phoneNumber", String(describing: phoneNumber)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("vcard", String(describing: vcard)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageMediaContact", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaGeo(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius, let replyMarkup): - return ("inputBotInlineMessageMediaGeo", [("flags", String(describing: flags)), ("geoPoint", String(describing: geoPoint)), ("heading", String(describing: heading)), ("period", String(describing: period)), ("proximityNotificationRadius", String(describing: proximityNotificationRadius)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageMediaGeo", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): - return ("inputBotInlineMessageMediaInvoice", [("flags", String(describing: flags)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("payload", String(describing: payload)), ("provider", String(describing: provider)), ("providerData", String(describing: providerData)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): - return ("inputBotInlineMessageMediaVenue", [("flags", String(describing: flags)), ("geoPoint", String(describing: geoPoint)), ("title", String(describing: title)), ("address", String(describing: address)), ("provider", String(describing: provider)), ("venueId", String(describing: venueId)), ("venueType", String(describing: venueType)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageMediaVenue", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): - return ("inputBotInlineMessageText", [("flags", String(describing: flags)), ("message", String(describing: message)), ("entities", String(describing: entities)), ("replyMarkup", String(describing: replyMarkup))]) + return ("inputBotInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) } } @@ -1216,9 +1216,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputBotInlineMessageID(let dcId, let id, let accessHash): - return ("inputBotInlineMessageID", [("dcId", String(describing: dcId)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputBotInlineMessageID", [("dcId", dcId as Any), ("id", id as Any), ("accessHash", accessHash as Any)]) case .inputBotInlineMessageID64(let dcId, let ownerId, let id, let accessHash): - return ("inputBotInlineMessageID64", [("dcId", String(describing: dcId)), ("ownerId", String(describing: ownerId)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputBotInlineMessageID64", [("dcId", dcId as Any), ("ownerId", ownerId as Any), ("id", id as Any), ("accessHash", accessHash as Any)]) } } @@ -1320,13 +1320,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputBotInlineResult(let flags, let id, let type, let title, let description, let url, let thumb, let content, let sendMessage): - return ("inputBotInlineResult", [("flags", String(describing: flags)), ("id", String(describing: id)), ("type", String(describing: type)), ("title", String(describing: title)), ("description", String(describing: description)), ("url", String(describing: url)), ("thumb", String(describing: thumb)), ("content", String(describing: content)), ("sendMessage", String(describing: sendMessage))]) + return ("inputBotInlineResult", [("flags", flags as Any), ("id", id as Any), ("type", type as Any), ("title", title as Any), ("description", description as Any), ("url", url as Any), ("thumb", thumb as Any), ("content", content as Any), ("sendMessage", sendMessage as Any)]) case .inputBotInlineResultDocument(let flags, let id, let type, let title, let description, let document, let sendMessage): - return ("inputBotInlineResultDocument", [("flags", String(describing: flags)), ("id", String(describing: id)), ("type", String(describing: type)), ("title", String(describing: title)), ("description", String(describing: description)), ("document", String(describing: document)), ("sendMessage", String(describing: sendMessage))]) + return ("inputBotInlineResultDocument", [("flags", flags as Any), ("id", id as Any), ("type", type as Any), ("title", title as Any), ("description", description as Any), ("document", document as Any), ("sendMessage", sendMessage as Any)]) case .inputBotInlineResultGame(let id, let shortName, let sendMessage): - return ("inputBotInlineResultGame", [("id", String(describing: id)), ("shortName", String(describing: shortName)), ("sendMessage", String(describing: sendMessage))]) + return ("inputBotInlineResultGame", [("id", id as Any), ("shortName", shortName as Any), ("sendMessage", sendMessage as Any)]) case .inputBotInlineResultPhoto(let id, let type, let photo, let sendMessage): - return ("inputBotInlineResultPhoto", [("id", String(describing: id)), ("type", String(describing: type)), ("photo", String(describing: photo)), ("sendMessage", String(describing: sendMessage))]) + return ("inputBotInlineResultPhoto", [("id", id as Any), ("type", type as Any), ("photo", photo as Any), ("sendMessage", sendMessage as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 14d86d4ec5b..a88a4dce080 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -33,11 +33,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputChannel(let channelId, let accessHash): - return ("inputChannel", [("channelId", String(describing: channelId)), ("accessHash", String(describing: accessHash))]) + return ("inputChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) case .inputChannelEmpty: return ("inputChannelEmpty", []) case .inputChannelFromMessage(let peer, let msgId, let channelId): - return ("inputChannelFromMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("channelId", String(describing: channelId))]) + return ("inputChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) } } @@ -115,11 +115,11 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputChatPhoto(let id): - return ("inputChatPhoto", [("id", String(describing: id))]) + return ("inputChatPhoto", [("id", id as Any)]) case .inputChatPhotoEmpty: return ("inputChatPhotoEmpty", []) case .inputChatUploadedPhoto(let flags, let file, let video, let videoStartTs): - return ("inputChatUploadedPhoto", [("flags", String(describing: flags)), ("file", String(describing: file)), ("video", String(describing: video)), ("videoStartTs", String(describing: videoStartTs))]) + return ("inputChatUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("video", video as Any), ("videoStartTs", videoStartTs as Any)]) } } @@ -195,7 +195,7 @@ public extension Api { case .inputCheckPasswordEmpty: return ("inputCheckPasswordEmpty", []) case .inputCheckPasswordSRP(let srpId, let A, let M1): - return ("inputCheckPasswordSRP", [("srpId", String(describing: srpId)), ("A", String(describing: A)), ("M1", String(describing: M1))]) + return ("inputCheckPasswordSRP", [("srpId", srpId as Any), ("A", A as Any), ("M1", M1 as Any)]) } } @@ -241,7 +241,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputClientProxy(let address, let port): - return ("inputClientProxy", [("address", String(describing: address)), ("port", String(describing: port))]) + return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) } } @@ -283,7 +283,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPhoneContact(let clientId, let phone, let firstName, let lastName): - return ("inputPhoneContact", [("clientId", String(describing: clientId)), ("phone", String(describing: phone)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName))]) + return ("inputPhoneContact", [("clientId", clientId as Any), ("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any)]) } } @@ -335,9 +335,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputDialogPeer(let peer): - return ("inputDialogPeer", [("peer", String(describing: peer))]) + return ("inputDialogPeer", [("peer", peer as Any)]) case .inputDialogPeerFolder(let folderId): - return ("inputDialogPeerFolder", [("folderId", String(describing: folderId))]) + return ("inputDialogPeerFolder", [("folderId", folderId as Any)]) } } @@ -395,7 +395,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputDocument(let id, let accessHash, let fileReference): - return ("inputDocument", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference))]) + return ("inputDocument", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any)]) case .inputDocumentEmpty: return ("inputDocumentEmpty", []) } @@ -443,7 +443,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputEncryptedChat(let chatId, let accessHash): - return ("inputEncryptedChat", [("chatId", String(describing: chatId)), ("accessHash", String(describing: accessHash))]) + return ("inputEncryptedChat", [("chatId", chatId as Any), ("accessHash", accessHash as Any)]) } } @@ -509,13 +509,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputEncryptedFile(let id, let accessHash): - return ("inputEncryptedFile", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputEncryptedFile", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputEncryptedFileBigUploaded(let id, let parts, let keyFingerprint): - return ("inputEncryptedFileBigUploaded", [("id", String(describing: id)), ("parts", String(describing: parts)), ("keyFingerprint", String(describing: keyFingerprint))]) + return ("inputEncryptedFileBigUploaded", [("id", id as Any), ("parts", parts as Any), ("keyFingerprint", keyFingerprint as Any)]) case .inputEncryptedFileEmpty: return ("inputEncryptedFileEmpty", []) case .inputEncryptedFileUploaded(let id, let parts, let md5Checksum, let keyFingerprint): - return ("inputEncryptedFileUploaded", [("id", String(describing: id)), ("parts", String(describing: parts)), ("md5Checksum", String(describing: md5Checksum)), ("keyFingerprint", String(describing: keyFingerprint))]) + return ("inputEncryptedFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("keyFingerprint", keyFingerprint as Any)]) } } @@ -606,9 +606,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputFile(let id, let parts, let name, let md5Checksum): - return ("inputFile", [("id", String(describing: id)), ("parts", String(describing: parts)), ("name", String(describing: name)), ("md5Checksum", String(describing: md5Checksum))]) + return ("inputFile", [("id", id as Any), ("parts", parts as Any), ("name", name as Any), ("md5Checksum", md5Checksum as Any)]) case .inputFileBig(let id, let parts, let name): - return ("inputFileBig", [("id", String(describing: id)), ("parts", String(describing: parts)), ("name", String(describing: name))]) + return ("inputFileBig", [("id", id as Any), ("parts", parts as Any), ("name", name as Any)]) } } @@ -757,23 +757,23 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputDocumentFileLocation(let id, let accessHash, let fileReference, let thumbSize): - return ("inputDocumentFileLocation", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("thumbSize", String(describing: thumbSize))]) + return ("inputDocumentFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) case .inputEncryptedFileLocation(let id, let accessHash): - return ("inputEncryptedFileLocation", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputEncryptedFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputFileLocation(let volumeId, let localId, let secret, let fileReference): - return ("inputFileLocation", [("volumeId", String(describing: volumeId)), ("localId", String(describing: localId)), ("secret", String(describing: secret)), ("fileReference", String(describing: fileReference))]) + return ("inputFileLocation", [("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any), ("fileReference", fileReference as Any)]) case .inputGroupCallStream(let flags, let call, let timeMs, let scale, let videoChannel, let videoQuality): - return ("inputGroupCallStream", [("flags", String(describing: flags)), ("call", String(describing: call)), ("timeMs", String(describing: timeMs)), ("scale", String(describing: scale)), ("videoChannel", String(describing: videoChannel)), ("videoQuality", String(describing: videoQuality))]) + return ("inputGroupCallStream", [("flags", flags as Any), ("call", call as Any), ("timeMs", timeMs as Any), ("scale", scale as Any), ("videoChannel", videoChannel as Any), ("videoQuality", videoQuality as Any)]) case .inputPeerPhotoFileLocation(let flags, let peer, let photoId): - return ("inputPeerPhotoFileLocation", [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("photoId", String(describing: photoId))]) + return ("inputPeerPhotoFileLocation", [("flags", flags as Any), ("peer", peer as Any), ("photoId", photoId as Any)]) case .inputPhotoFileLocation(let id, let accessHash, let fileReference, let thumbSize): - return ("inputPhotoFileLocation", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("thumbSize", String(describing: thumbSize))]) + return ("inputPhotoFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("thumbSize", thumbSize as Any)]) case .inputPhotoLegacyFileLocation(let id, let accessHash, let fileReference, let volumeId, let localId, let secret): - return ("inputPhotoLegacyFileLocation", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("volumeId", String(describing: volumeId)), ("localId", String(describing: localId)), ("secret", String(describing: secret))]) + return ("inputPhotoLegacyFileLocation", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any), ("volumeId", volumeId as Any), ("localId", localId as Any), ("secret", secret as Any)]) case .inputSecureFileLocation(let id, let accessHash): - return ("inputSecureFileLocation", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputSecureFileLocation", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputStickerSetThumb(let stickerset, let thumbVersion): - return ("inputStickerSetThumb", [("stickerset", String(describing: stickerset)), ("thumbVersion", String(describing: thumbVersion))]) + return ("inputStickerSetThumb", [("stickerset", stickerset as Any), ("thumbVersion", thumbVersion as Any)]) case .inputTakeoutFileLocation: return ("inputTakeoutFileLocation", []) } @@ -981,7 +981,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputFolderPeer(let peer, let folderId): - return ("inputFolderPeer", [("peer", String(describing: peer)), ("folderId", String(describing: folderId))]) + return ("inputFolderPeer", [("peer", peer as Any), ("folderId", folderId as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 89c644924e9..67f940676e2 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -25,9 +25,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputGameID(let id, let accessHash): - return ("inputGameID", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputGameID", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputGameShortName(let botId, let shortName): - return ("inputGameShortName", [("botId", String(describing: botId)), ("shortName", String(describing: shortName))]) + return ("inputGameShortName", [("botId", botId as Any), ("shortName", shortName as Any)]) } } @@ -92,7 +92,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputGeoPoint(let flags, let lat, let long, let accuracyRadius): - return ("inputGeoPoint", [("flags", String(describing: flags)), ("lat", String(describing: lat)), ("long", String(describing: long)), ("accuracyRadius", String(describing: accuracyRadius))]) + return ("inputGeoPoint", [("flags", flags as Any), ("lat", lat as Any), ("long", long as Any), ("accuracyRadius", accuracyRadius as Any)]) case .inputGeoPointEmpty: return ("inputGeoPointEmpty", []) } @@ -143,7 +143,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputGroupCall(let id, let accessHash): - return ("inputGroupCall", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputGroupCall", [("id", id as Any), ("accessHash", accessHash as Any)]) } } @@ -190,9 +190,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputInvoiceMessage(let peer, let msgId): - return ("inputInvoiceMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId))]) + return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) case .inputInvoiceSlug(let slug): - return ("inputInvoiceSlug", [("slug", String(describing: slug))]) + return ("inputInvoiceSlug", [("slug", slug as Any)]) } } @@ -405,35 +405,35 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputMediaContact(let phoneNumber, let firstName, let lastName, let vcard): - return ("inputMediaContact", [("phoneNumber", String(describing: phoneNumber)), ("firstName", String(describing: firstName)), ("lastName", String(describing: lastName)), ("vcard", String(describing: vcard))]) + return ("inputMediaContact", [("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any)]) case .inputMediaDice(let emoticon): - return ("inputMediaDice", [("emoticon", String(describing: emoticon))]) + return ("inputMediaDice", [("emoticon", emoticon as Any)]) case .inputMediaDocument(let flags, let id, let ttlSeconds, let query): - return ("inputMediaDocument", [("flags", String(describing: flags)), ("id", String(describing: id)), ("ttlSeconds", String(describing: ttlSeconds)), ("query", String(describing: query))]) + return ("inputMediaDocument", [("flags", flags as Any), ("id", id as Any), ("ttlSeconds", ttlSeconds as Any), ("query", query as Any)]) case .inputMediaDocumentExternal(let flags, let url, let ttlSeconds): - return ("inputMediaDocumentExternal", [("flags", String(describing: flags)), ("url", String(describing: url)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("inputMediaDocumentExternal", [("flags", flags as Any), ("url", url as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaEmpty: return ("inputMediaEmpty", []) case .inputMediaGame(let id): - return ("inputMediaGame", [("id", String(describing: id))]) + return ("inputMediaGame", [("id", id as Any)]) case .inputMediaGeoLive(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius): - return ("inputMediaGeoLive", [("flags", String(describing: flags)), ("geoPoint", String(describing: geoPoint)), ("heading", String(describing: heading)), ("period", String(describing: period)), ("proximityNotificationRadius", String(describing: proximityNotificationRadius))]) + return ("inputMediaGeoLive", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)]) case .inputMediaGeoPoint(let geoPoint): - return ("inputMediaGeoPoint", [("geoPoint", String(describing: geoPoint))]) + return ("inputMediaGeoPoint", [("geoPoint", geoPoint as Any)]) case .inputMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let startParam, let extendedMedia): - return ("inputMediaInvoice", [("flags", String(describing: flags)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("payload", String(describing: payload)), ("provider", String(describing: provider)), ("providerData", String(describing: providerData)), ("startParam", String(describing: startParam)), ("extendedMedia", String(describing: extendedMedia))]) + return ("inputMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) case .inputMediaPhoto(let flags, let id, let ttlSeconds): - return ("inputMediaPhoto", [("flags", String(describing: flags)), ("id", String(describing: id)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("inputMediaPhoto", [("flags", flags as Any), ("id", id as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaPhotoExternal(let flags, let url, let ttlSeconds): - return ("inputMediaPhotoExternal", [("flags", String(describing: flags)), ("url", String(describing: url)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("inputMediaPhotoExternal", [("flags", flags as Any), ("url", url as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities): - return ("inputMediaPoll", [("flags", String(describing: flags)), ("poll", String(describing: poll)), ("correctAnswers", String(describing: correctAnswers)), ("solution", String(describing: solution)), ("solutionEntities", String(describing: solutionEntities))]) + return ("inputMediaPoll", [("flags", flags as Any), ("poll", poll as Any), ("correctAnswers", correctAnswers as Any), ("solution", solution as Any), ("solutionEntities", solutionEntities as Any)]) case .inputMediaUploadedDocument(let flags, let file, let thumb, let mimeType, let attributes, let stickers, let ttlSeconds): - return ("inputMediaUploadedDocument", [("flags", String(describing: flags)), ("file", String(describing: file)), ("thumb", String(describing: thumb)), ("mimeType", String(describing: mimeType)), ("attributes", String(describing: attributes)), ("stickers", String(describing: stickers)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("inputMediaUploadedDocument", [("flags", flags as Any), ("file", file as Any), ("thumb", thumb as Any), ("mimeType", mimeType as Any), ("attributes", attributes as Any), ("stickers", stickers as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaUploadedPhoto(let flags, let file, let stickers, let ttlSeconds): - return ("inputMediaUploadedPhoto", [("flags", String(describing: flags)), ("file", String(describing: file)), ("stickers", String(describing: stickers)), ("ttlSeconds", String(describing: ttlSeconds))]) + return ("inputMediaUploadedPhoto", [("flags", flags as Any), ("file", file as Any), ("stickers", stickers as Any), ("ttlSeconds", ttlSeconds as Any)]) case .inputMediaVenue(let geoPoint, let title, let address, let provider, let venueId, let venueType): - return ("inputMediaVenue", [("geoPoint", String(describing: geoPoint)), ("title", String(describing: title)), ("address", String(describing: address)), ("provider", String(describing: provider)), ("venueId", String(describing: venueId)), ("venueType", String(describing: venueType))]) + return ("inputMediaVenue", [("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) } } @@ -804,13 +804,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputMessageCallbackQuery(let id, let queryId): - return ("inputMessageCallbackQuery", [("id", String(describing: id)), ("queryId", String(describing: queryId))]) + return ("inputMessageCallbackQuery", [("id", id as Any), ("queryId", queryId as Any)]) case .inputMessageID(let id): - return ("inputMessageID", [("id", String(describing: id))]) + return ("inputMessageID", [("id", id as Any)]) case .inputMessagePinned: return ("inputMessagePinned", []) case .inputMessageReplyTo(let id): - return ("inputMessageReplyTo", [("id", String(describing: id))]) + return ("inputMessageReplyTo", [("id", id as Any)]) } } @@ -907,9 +907,9 @@ public extension Api { case .inputNotifyChats: return ("inputNotifyChats", []) case .inputNotifyForumTopic(let peer, let topMsgId): - return ("inputNotifyForumTopic", [("peer", String(describing: peer)), ("topMsgId", String(describing: topMsgId))]) + return ("inputNotifyForumTopic", [("peer", peer as Any), ("topMsgId", topMsgId as Any)]) case .inputNotifyPeer(let peer): - return ("inputNotifyPeer", [("peer", String(describing: peer))]) + return ("inputNotifyPeer", [("peer", peer as Any)]) case .inputNotifyUsers: return ("inputNotifyUsers", []) } @@ -997,13 +997,13 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPaymentCredentials(let flags, let data): - return ("inputPaymentCredentials", [("flags", String(describing: flags)), ("data", String(describing: data))]) + return ("inputPaymentCredentials", [("flags", flags as Any), ("data", data as Any)]) case .inputPaymentCredentialsApplePay(let paymentData): - return ("inputPaymentCredentialsApplePay", [("paymentData", String(describing: paymentData))]) + return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData as Any)]) case .inputPaymentCredentialsGooglePay(let paymentToken): - return ("inputPaymentCredentialsGooglePay", [("paymentToken", String(describing: paymentToken))]) + return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken as Any)]) case .inputPaymentCredentialsSaved(let id, let tmpPassword): - return ("inputPaymentCredentialsSaved", [("id", String(describing: id)), ("tmpPassword", String(describing: tmpPassword))]) + return ("inputPaymentCredentialsSaved", [("id", id as Any), ("tmpPassword", tmpPassword as Any)]) } } diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index dab267e25bc..4c60320822a 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -64,19 +64,19 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPeerChannel(let channelId, let accessHash): - return ("inputPeerChannel", [("channelId", String(describing: channelId)), ("accessHash", String(describing: accessHash))]) + return ("inputPeerChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): - return ("inputPeerChannelFromMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("channelId", String(describing: channelId))]) + return ("inputPeerChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) case .inputPeerChat(let chatId): - return ("inputPeerChat", [("chatId", String(describing: chatId))]) + return ("inputPeerChat", [("chatId", chatId as Any)]) case .inputPeerEmpty: return ("inputPeerEmpty", []) case .inputPeerSelf: return ("inputPeerSelf", []) case .inputPeerUser(let userId, let accessHash): - return ("inputPeerUser", [("userId", String(describing: userId)), ("accessHash", String(describing: accessHash))]) + return ("inputPeerUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) case .inputPeerUserFromMessage(let peer, let msgId, let userId): - return ("inputPeerUserFromMessage", [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("userId", String(describing: userId))]) + return ("inputPeerUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) } } @@ -188,7 +188,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPeerNotifySettings(let flags, let showPreviews, let silent, let muteUntil, let sound): - return ("inputPeerNotifySettings", [("flags", String(describing: flags)), ("showPreviews", String(describing: showPreviews)), ("silent", String(describing: silent)), ("muteUntil", String(describing: muteUntil)), ("sound", String(describing: sound))]) + return ("inputPeerNotifySettings", [("flags", flags as Any), ("showPreviews", showPreviews as Any), ("silent", silent as Any), ("muteUntil", muteUntil as Any), ("sound", sound as Any)]) } } @@ -243,7 +243,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPhoneCall(let id, let accessHash): - return ("inputPhoneCall", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputPhoneCall", [("id", id as Any), ("accessHash", accessHash as Any)]) } } @@ -291,7 +291,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputPhoto(let id, let accessHash, let fileReference): - return ("inputPhoto", [("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference))]) + return ("inputPhoto", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any)]) case .inputPhotoEmpty: return ("inputPhotoEmpty", []) } @@ -529,19 +529,19 @@ public extension Api { case .inputPrivacyValueAllowAll: return ("inputPrivacyValueAllowAll", []) case .inputPrivacyValueAllowChatParticipants(let chats): - return ("inputPrivacyValueAllowChatParticipants", [("chats", String(describing: chats))]) + return ("inputPrivacyValueAllowChatParticipants", [("chats", chats as Any)]) case .inputPrivacyValueAllowContacts: return ("inputPrivacyValueAllowContacts", []) case .inputPrivacyValueAllowUsers(let users): - return ("inputPrivacyValueAllowUsers", [("users", String(describing: users))]) + return ("inputPrivacyValueAllowUsers", [("users", users as Any)]) case .inputPrivacyValueDisallowAll: return ("inputPrivacyValueDisallowAll", []) case .inputPrivacyValueDisallowChatParticipants(let chats): - return ("inputPrivacyValueDisallowChatParticipants", [("chats", String(describing: chats))]) + return ("inputPrivacyValueDisallowChatParticipants", [("chats", chats as Any)]) case .inputPrivacyValueDisallowContacts: return ("inputPrivacyValueDisallowContacts", []) case .inputPrivacyValueDisallowUsers(let users): - return ("inputPrivacyValueDisallowUsers", [("users", String(describing: users))]) + return ("inputPrivacyValueDisallowUsers", [("users", users as Any)]) } } @@ -642,9 +642,9 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputSecureFile(let id, let accessHash): - return ("inputSecureFile", [("id", String(describing: id)), ("accessHash", String(describing: accessHash))]) + return ("inputSecureFile", [("id", id as Any), ("accessHash", accessHash as Any)]) case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): - return ("inputSecureFileUploaded", [("id", String(describing: id)), ("parts", String(describing: parts)), ("md5Checksum", String(describing: md5Checksum)), ("fileHash", String(describing: fileHash)), ("secret", String(describing: secret))]) + return ("inputSecureFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) } } @@ -722,7 +722,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): - return ("inputSecureValue", [("flags", String(describing: flags)), ("type", String(describing: type)), ("data", String(describing: data)), ("frontSide", String(describing: frontSide)), ("reverseSide", String(describing: reverseSide)), ("selfie", String(describing: selfie)), ("translation", String(describing: translation)), ("files", String(describing: files)), ("plainData", String(describing: plainData))]) + return ("inputSecureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any)]) } } @@ -806,7 +806,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { case .inputSingleMedia(let flags, let media, let randomId, let message, let entities): - return ("inputSingleMedia", [("flags", String(describing: flags)), ("media", String(describing: media)), ("randomId", String(describing: randomId)), ("message", String(describing: message)), ("entities", String(describing: entities))]) + return ("inputSingleMedia", [("flags", flags as Any), ("media", media as Any), ("randomId", randomId as Any), ("message", message as Any), ("entities", entities as Any)]) } } diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 2c0f4c02276..eb42a04996a 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -209,6 +209,7 @@ public final class ManagedAudioSession { private let isActiveSubscribers = Bag<(Bool) -> Void>() private let isPlaybackActiveSubscribers = Bag<(Bool) -> Void>() + private var isActiveValue: Bool = false private var callKitAudioSessionIsActive: Bool = false public init() { @@ -392,7 +393,7 @@ public final class ManagedAudioSession { let queue = self.queue return Signal { [weak self] subscriber in if let strongSelf = self { - subscriber.putNext(strongSelf.currentTypeAndOutputMode != nil) + subscriber.putNext(strongSelf.isActiveValue || strongSelf.callKitAudioSessionIsActive) let index = strongSelf.isActiveSubscribers.add({ value in subscriber.putNext(value) @@ -686,7 +687,6 @@ public final class ManagedAudioSession { self.deactivateTimer?.invalidate() self.deactivateTimer = nil - let wasActive = self.currentTypeAndOutputMode != nil let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false self.currentTypeAndOutputMode = nil @@ -709,10 +709,9 @@ public final class ManagedAudioSession { } } - if wasActive { - for subscriber in self.isActiveSubscribers.copyItems() { - subscriber(false) - } + self.isActiveValue = false + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(self.isActiveValue || self.callKitAudioSessionIsActive) } if wasPlaybackActive { for subscriber in self.isPlaybackActiveSubscribers.copyItems() { @@ -725,7 +724,6 @@ public final class ManagedAudioSession { self.deactivateTimer?.invalidate() self.deactivateTimer = nil - let wasActive = self.currentTypeAndOutputMode != nil let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) { @@ -782,10 +780,9 @@ public final class ManagedAudioSession { } } - if !wasActive { - for subscriber in self.isActiveSubscribers.copyItems() { - subscriber(true) - } + self.isActiveValue = true + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(self.isActiveValue || self.callKitAudioSessionIsActive) } if !wasPlaybackActive && (self.currentTypeAndOutputMode?.0.isPlay ?? false) { for subscriber in self.isPlaybackActiveSubscribers.copyItems() { @@ -976,6 +973,10 @@ public final class ManagedAudioSession { managedAudioSessionLog("ManagedAudioSession callKitActivatedAudioSession") self.callKitAudioSessionIsActive = true self.updateHolders() + + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(self.isActiveValue || self.callKitAudioSessionIsActive) + } } } @@ -984,6 +985,10 @@ public final class ManagedAudioSession { managedAudioSessionLog("ManagedAudioSession callKitDeactivatedAudioSession") self.callKitAudioSessionIsActive = false self.updateHolders() + + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(self.isActiveValue || self.callKitAudioSessionIsActive) + } } } } diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index a5e890f3b65..470c190e16c 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -85,6 +85,7 @@ public final class PresentationCallImpl: PresentationCall { private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil) private var currentAudioOutputValue: AudioSessionOutput = .builtin + private var didSetCurrentAudioOutputValue: Bool = false public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { return self.audioOutputStatePromise.get() } @@ -242,6 +243,7 @@ public final class PresentationCallImpl: PresentationCall { strongSelf.audioOutputStateValue = (availableOutputs, currentOutput) if let currentOutput = currentOutput { strongSelf.currentAudioOutputValue = currentOutput + strongSelf.didSetCurrentAudioOutputValue = true } var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) @@ -371,7 +373,9 @@ public final class PresentationCallImpl: PresentationCall { if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil { if let callKitIntegration = self.callKitIntegration { - callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + if self.didSetCurrentAudioOutputValue { + callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) + } } else { audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) audioSessionControl.setup(synchronous: true) @@ -868,6 +872,7 @@ public final class PresentationCallImpl: PresentationCall { return } self.currentAudioOutputValue = output + self.didSetCurrentAudioOutputValue = true self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output)) |> then( @@ -888,6 +893,10 @@ public final class PresentationCallImpl: PresentationCall { return self.debugInfoValue.get() } + func video(isIncoming: Bool) -> Signal? { + return self.ongoingContext?.video(isIncoming: isIncoming) + } + public func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { self.ongoingContext?.makeIncomingVideoView(completion: { view in if let view = view { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 0017fdb16ba..31a2d6ccf19 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1254,7 +1254,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil, duration: 3), action: { _ in return false }) } } else { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { @@ -1362,7 +1362,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } else if case let .legacyGroup(groupPeer) = groupPeer { @@ -1430,7 +1430,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } @@ -2262,7 +2262,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController return } let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(EnginePeer(event.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(event.peer), text: text, action: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(event.peer), text: text, action: nil, duration: 3), action: { _ in return false }) } })) @@ -2277,7 +2277,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(peer), text: text, action: nil), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(peer), text: text, action: nil, duration: 3), action: { _ in return false }) })) self.stateVersionDisposable.set((self.call.stateVersion @@ -6154,18 +6154,8 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) -// paintStickersContext.presentStickersController = { completion in -// let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in -// let coder = PostboxEncoder() -// coder.encodeRootObject(fileReference.media) -// completion?(coder.makeData(), fileReference.media.isAnimatedSticker, node.view, rect) -// return true -// }) -// strongSelf.controller?.present(controller, in: .window(.root)) -// return controller -// } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! mixin.forceDark = true mixin.stickersContext = paintStickersContext let _ = strongSelf.currentAvatarMixin.swap(mixin) @@ -6251,7 +6241,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = representation self.updateAvatarPromise.set(.single((representation, 0.0))) @@ -6286,7 +6276,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = representation self.updateAvatarPromise.set(.single((representation, 0.0))) @@ -6308,9 +6298,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal - if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { let durationSignal: SSignal = SSignal(generator: { subscriber in let disposable = (entityRenderer.duration()).start(next: { duration in subscriber.putNext(duration) @@ -6329,6 +6317,8 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } }) + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! } else { signal = SSignal.complete() } diff --git a/submodules/TelegramCore/BUILD b/submodules/TelegramCore/BUILD index 7f444963cdf..5a883c93563 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/Reachability:Reachability", "//submodules/ManagedFile:ManagedFile", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/Utils/DarwinDirStat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCore/Package.swift b/submodules/TelegramCore/Package.swift index 31a9d13bba3..0f5dd02e8bd 100644 --- a/submodules/TelegramCore/Package.swift +++ b/submodules/TelegramCore/Package.swift @@ -22,6 +22,7 @@ let package = Package( .package(name: "CryptoUtils", path: "../CryptoUtils"), .package(name: "NetworkLogging", path: "../NetworkLogging"), .package(name: "Reachability", path: "../Reachability"), + .package(name: "DarwinDirStat", path: "../Utils/DarwinDirStat"), .package(name: "EncryptionProvider", path: "../EncryptionProvider"), ], targets: [ @@ -35,6 +36,7 @@ let package = Package( .product(name: "TelegramApi", package: "TelegramApi", condition: nil), .product(name: "CryptoUtils", package: "CryptoUtils", condition: nil), .product(name: "NetworkLogging", package: "NetworkLogging", condition: nil), + .product(name: "DarwinDirStat", package: "DarwinDirStat", condition: nil), .product(name: "Reachability", package: "Reachability", condition: nil), .product(name: "EncryptionProvider", package: "EncryptionProvider", condition: nil)], path: "Sources"), diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index c6f6025a61a..ad10081bac8 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -927,6 +927,7 @@ public class Account { private let managedOperationsDisposable = DisposableSet() private let managedTopReactionsDisposable = MetaDisposable() private var storageSettingsDisposable: Disposable? + private var automaticCacheEvictionContext: AutomaticCacheEvictionContext? public let importableContacts = Promise<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]>() @@ -1241,7 +1242,8 @@ public class Account { if !supplementary { let mediaBox = postbox.mediaBox - self.storageSettingsDisposable = accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]).start(next: { [weak mediaBox] sharedData in + let _ = (accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> take(1)).start(next: { [weak mediaBox] sharedData in guard let mediaBox = mediaBox else { return } @@ -1269,6 +1271,8 @@ public class Account { strongSelf.managedTopReactionsDisposable.set(managedTopReactions(postbox: strongSelf.postbox, network: strongSelf.network).start()) } + self.automaticCacheEvictionContext = AutomaticCacheEvictionContext(postbox: postbox, accountManager: accountManager) + /*#if DEBUG self.managedOperationsDisposable.add(debugFetchAllStickers(account: self).start(completed: { print("debugFetchAllStickers done") @@ -1341,6 +1345,19 @@ public class Account { self.viewTracker.reset() } + public func cleanupTasks(lowImpact: Bool) -> Signal { + let postbox = self.postbox + + return _internal_reindexCacheInBackground(account: self, lowImpact: lowImpact) + |> then( + Signal { subscriber in + return postbox.mediaBox.updateResourceIndex(lowImpact: lowImpact, completion: { + subscriber.putCompletion() + }) + } + ) + } + public func restartContactManagement() { self.contactSyncManager.beginSync(importableContacts: self.importableContacts.get()) } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 6ebaef1358c..27617943f11 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -276,6 +276,7 @@ private var declaredEncodables: Void = { declareEncodable(NonPremiumMessageAttribute.self, f: { NonPremiumMessageAttribute(decoder: $0) }) declareEncodable(TelegramExtendedMedia.self, f: { TelegramExtendedMedia(decoder: $0) }) declareEncodable(TelegramPeerUsername.self, f: { TelegramPeerUsername(decoder: $0) }) + declareEncodable(MediaSpoilerMessageAttribute.self, f: { MediaSpoilerMessageAttribute(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 8eb613d13c7..b245d6d68e7 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -14,8 +14,8 @@ func imageRepresentationsForApiChatPhoto(_ photo: Api.ChatPhoto) -> [TelegramMed smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .small, volumeId: nil, localId: nil) fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .fullSize, volumeId: nil, localId: nil) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: false)) case .chatPhotoEmpty: break } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift index 11f06f6a33d..1d933fcf282 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ChatContextResult.swift @@ -535,3 +535,48 @@ extension ChatContextResultCollection { } } } + +public func requestContextResults(engine: TelegramEngine, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal { + return engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) + |> `catch` { error -> Signal in + return .single(nil) + } + |> mapToSignal { resultsStruct -> Signal in + let results = resultsStruct?.results + + var collection = existingResults + var updated: Bool = false + if let existingResults = existingResults, let results = results { + var newResults: [ChatContextResult] = [] + var existingIds = Set() + for result in existingResults.results { + newResults.append(result) + existingIds.insert(result.id) + } + for result in results.results { + if !existingIds.contains(result.id) { + newResults.append(result) + existingIds.insert(result.id) + updated = true + } + } + collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, results: newResults, cacheTimeout: existingResults.cacheTimeout) + } else { + collection = results + updated = true + } + if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated { + let nextResults = requestContextResults(engine: engine, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit) + if collection.results.count > 10 { + return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false)) + |> then(nextResults) + } else { + return nextResults + } + } else if let collection = collection { + return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false)) + } else { + return .single(nil) + } + } +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 3a97dfaaf46..26659984f56 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -76,6 +76,9 @@ extension ReplyMarkupMessageAttribute { if (markupFlags & (1 << 2)) != 0 { flags.insert(.personal) } + if (markupFlags & (1 << 4)) != 0 { + flags.insert(.persistent) + } placeholder = apiPlaceholder case let .replyInlineMarkup(apiRows): rows = apiRows.map { ReplyMarkupRow(apiRow: $0) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index ceaf3c6d370..fc1aa0a66b3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -205,7 +205,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionAttachMenuBotAllowed: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) @@ -266,48 +266,48 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere return nil } -func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (Media?, Int32?, Bool?) { +func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (Media?, Int32?, Bool?, Bool?) { if let media = media { switch media { - case let .messageMediaPhoto(_, photo, ttlSeconds): + case let .messageMediaPhoto(flags, photo, ttlSeconds): if let photo = photo { if let mediaImage = telegramMediaImageFromApiPhoto(photo) { - return (mediaImage, ttlSeconds, nil) + return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0) } } else { - return (TelegramMediaExpiredContent(data: .image), nil, nil) + return (TelegramMediaExpiredContent(data: .image), nil, nil, nil) } case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId): let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard) - return (mediaContact, nil, nil) + return (mediaContact, nil, nil, nil) case let .messageMediaGeo(geo): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil) + return (mediaMap, nil, nil, nil) case let .messageMediaVenue(geo, title, address, provider, venueId, venueType): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil) - return (mediaMap, nil, nil) + return (mediaMap, nil, nil, nil) case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius): let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading) - return (mediaMap, nil, nil) + return (mediaMap, nil, nil, nil) case let .messageMediaDocument(flags, document, ttlSeconds): if let document = document { if let mediaFile = telegramMediaFileFromApiDocument(document) { - return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0) + return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0) } } else { - return (TelegramMediaExpiredContent(data: .file), nil, nil) + return (TelegramMediaExpiredContent(data: .file), nil, nil, nil) } case let .messageMediaWebPage(webpage): if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage, url: nil) { - return (mediaWebpage, nil, nil) + return (mediaWebpage, nil, nil, nil) } case .messageMediaUnsupported: - return (TelegramMediaUnsupported(), nil, nil) + return (TelegramMediaUnsupported(), nil, nil, nil) case .messageMediaEmpty: break case let .messageMediaGame(game): - return (TelegramMediaGame(apiGame: game), nil, nil) + return (TelegramMediaGame(apiGame: game), nil, nil, nil) case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia): var parsedFlags = TelegramMediaInvoiceFlags() if (flags & (1 << 3)) != 0 { @@ -331,7 +331,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) + let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId) if let media = media { extendedMedia = .full(media: media) } else { @@ -342,7 +342,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI extendedMedia = nil } - return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil) + return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil) case let .messageMediaPoll(poll, results): switch poll { case let .poll(id, flags, question, answers, closePeriod, _): @@ -358,14 +358,14 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil) } case let .messageMediaDice(value, emoticon): - return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil) + return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil) } } - return (nil, nil, nil) + return (nil, nil, nil, nil) } func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [MessageTextEntity] { @@ -534,19 +534,22 @@ extension StoreMessage { var consumableContent: (Bool, Bool)? = nil if let media = media { - let (mediaValue, expirationTimer, nonPremium) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) if let expirationTimer = expirationTimer, expirationTimer > 0 { attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil)) - consumableContent = (true, false) } if let nonPremium = nonPremium, nonPremium { attributes.append(NonPremiumMessageAttribute()) } + + if let hasSpoiler = hasSpoiler, hasSpoiler { + attributes.append(MediaSpoilerMessageAttribute()) + } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 1bccf451f46..35aad645f23 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -104,6 +104,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe components.append(.isHidden(hidden == .boolTrue)) } return TelegramMediaAction(action: .topicEdited(components: components)) + case let.messageActionSuggestProfilePhoto(photo): + return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo))) + case .messageActionAttachMenuBotAllowed: + return TelegramMediaAction(action: .attachMenuBotAllowed) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 3d4bf05c996..99f83f6aba3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -112,7 +112,8 @@ func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAtt result.append(.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer)) case let .documentAttributeCustomEmoji(flags, alt, stickerSet): let isFree = (flags & (1 << 0)) != 0 - result.append(.CustomEmoji(isPremium: !isFree, alt: alt, packReference: StickerPackReference(apiInputSet: stickerSet))) + let isSingleColor = (flags & (1 << 1)) != 0 + result.append(.CustomEmoji(isPremium: !isFree, isSingleColor: isSingleColor, alt: alt, packReference: StickerPackReference(apiInputSet: stickerSet))) } } return result @@ -134,13 +135,13 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32, switch size { case let .photoCachedSize(type, w, h, _): let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSize(type, w, h, _): let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSizeProgressive(type, w, h, sizes): let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoPathSize(_, data): immediateThumbnailData = data.makeData() case let .photoStrippedSize(_, data): diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift index 662e5678bb3..f12e13eec1f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaImage.swift @@ -10,14 +10,14 @@ func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId: switch size { case let .photoCachedSize(type, w, h, _): let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: nil, fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSize(type, w, h, size): let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSizeProgressive(type, w, h, sizes): if !sizes.isEmpty { let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int64(sizes[sizes.count - 1]), fileReference: fileReference) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } case let .photoStrippedSize(_, data): immediateThumbnailData = data.makeData() diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 3a06d1ff765..c3eb03752bc 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -8,6 +8,7 @@ func parsedTelegramProfilePhoto(_ photo: Api.UserProfilePhoto) -> [TelegramMedia switch photo { case let .userProfilePhoto(flags, id, strippedThumb, dcId): let hasVideo = (flags & (1 << 0)) != 0 + let isPersonal = (flags & (1 << 2)) != 0 let smallResource: TelegramMediaResource let fullSizeResource: TelegramMediaResource @@ -15,8 +16,8 @@ func parsedTelegramProfilePhoto(_ photo: Api.UserProfilePhoto) -> [TelegramMedia smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .small, volumeId: nil, localId: nil) fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .fullSize, volumeId: nil, localId: nil) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: isPersonal)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: isPersonal)) case .userProfilePhotoEmpty: break } @@ -97,8 +98,10 @@ extension TelegramUser { if !isMin { return TelegramUser(user: rhs) } else { + let applyMinPhoto = (flags & (1 << 25)) != 0 + let telegramPhoto: [TelegramMediaImageRepresentation] - if let photo = photo { + if let photo = photo, applyMinPhoto { telegramPhoto = parsedTelegramProfilePhoto(photo) } else if let currentPhoto = lhs?.photo { telegramPhoto = currentPhoto diff --git a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift index 92b139dffe9..b0ae253b1aa 100644 --- a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift @@ -40,7 +40,7 @@ public func requestUpdatesXml(account: Account, source: String) -> Signal, MediaBoxFetchPriority)? = nil, statsCategory: MediaResourceStatsCategory = .generic, reportResultStatus: Bool = false, preferBackgroundReferenceRevalidation: Bool = false, continueInBackground: Bool = false) -> Signal { - return fetchedMediaResource(mediaBox: mediaBox, reference: reference, ranges: range.flatMap({ [$0] }), statsCategory: statsCategory, reportResultStatus: reportResultStatus, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground) +public func fetchedMediaResource( + mediaBox: MediaBox, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, + reference: MediaResourceReference, + range: (Range, MediaBoxFetchPriority)? = nil, + statsCategory: MediaResourceStatsCategory = .generic, + reportResultStatus: Bool = false, + preferBackgroundReferenceRevalidation: Bool = false, + continueInBackground: Bool = false +) -> Signal { + return fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: reference, ranges: range.flatMap({ [$0] }), statsCategory: statsCategory, reportResultStatus: reportResultStatus, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground) } -public func fetchedMediaResource(mediaBox: MediaBox, reference: MediaResourceReference, ranges: [(Range, MediaBoxFetchPriority)]?, statsCategory: MediaResourceStatsCategory = .generic, reportResultStatus: Bool = false, preferBackgroundReferenceRevalidation: Bool = false, continueInBackground: Bool = false) -> Signal { +public extension MediaResourceStorageLocation { + convenience init?(userLocation: MediaResourceUserLocation, reference: MediaResourceReference) { + switch reference { + case let .media(media, _): + switch media { + case let .message(message, _): + if let id = message.id { + self.init(peerId: id.peerId, messageId: id) + return + } + default: + break + } + default: + break + } + + switch userLocation { + case let .peer(id): + self.init(peerId: id, messageId: nil) + case .other: + return nil + } + } +} + +public enum MediaResourceUserLocation: Equatable { + case peer(EnginePeer.Id) + case other +} + +public func fetchedMediaResource( + mediaBox: MediaBox, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, + reference: MediaResourceReference, + ranges: [(Range, MediaBoxFetchPriority)]?, + statsCategory: MediaResourceStatsCategory = .generic, + reportResultStatus: Bool = false, + preferBackgroundReferenceRevalidation: Bool = false, + continueInBackground: Bool = false +) -> Signal { var isRandomAccessAllowed = true switch reference { case let .media(media, _): @@ -42,16 +93,30 @@ public func fetchedMediaResource(mediaBox: MediaBox, reference: MediaResourceRef break } + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: reference) + if let ranges = ranges { let signals = ranges.map { (range, priority) -> Signal in - return mediaBox.fetchedResourceData(reference.resource, in: range, priority: priority, parameters: MediaResourceFetchParameters(tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), isRandomAccessAllowed: isRandomAccessAllowed)) + return mediaBox.fetchedResourceData(reference.resource, in: range, priority: priority, parameters: MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), + location: location, + contentType: userContentType, + isRandomAccessAllowed: isRandomAccessAllowed + )) } return combineLatest(signals) |> ignoreValues |> map { _ -> FetchResourceSourceType in } |> then(.single(.local)) } else { - return mediaBox.fetchedResource(reference.resource, parameters: MediaResourceFetchParameters(tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), isRandomAccessAllowed: isRandomAccessAllowed), implNext: reportResultStatus) + return mediaBox.fetchedResource(reference.resource, parameters: MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: statsCategory), + info: TelegramCloudMediaResourceFetchInfo(reference: reference, preferBackgroundReferenceRevalidation: preferBackgroundReferenceRevalidation, continueInBackground: continueInBackground), + location: location, + contentType: userContentType, + isRandomAccessAllowed: isRandomAccessAllowed + ), implNext: reportResultStatus) } } @@ -135,6 +200,10 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { return result } + case let .suggestedProfilePhoto(image): + if let image = image, let result = findMediaResource(media: image, previousMedia: previousMedia, resource: resource) { + return result + } default: break } @@ -200,6 +269,10 @@ func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> Telegra if let image = image, let result = findMediaResourceById(media: image, resourceId: resourceId) { return result } + case let .suggestedProfilePhoto(image): + if let image = image, let result = findMediaResourceById(media: image, resourceId: resourceId) { + return result + } default: break } diff --git a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift index e75d0afc6b8..36ca2f18db3 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartFetch.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartFetch.swift @@ -532,18 +532,62 @@ private final class MultipartFetchManager { self.reportCompleteSize = reportCompleteSize self.finishWithError = finishWithError - self.rangesDisposable = (intervals - |> deliverOn(self.queue)).start(next: { [weak self] intervals in - if let strongSelf = self { - if let _ = strongSelf.currentIntervals { - strongSelf.currentIntervals = intervals - strongSelf.checkState() - } else { - strongSelf.currentIntervals = intervals - strongSelf.checkState() + /*if resource.id.stringRepresentation == "telegram-cloud-document-1-4922941873166746479" { + let tempRanges = Promise<[(Range, MediaBoxFetchPriority)]>() + + tempRanges.set(.single([(0 ..< Int64.max, .default)])) + Thread(block: { + for i in 0 ..< 10 { + Thread.sleep(forTimeInterval: 0.1) + + var randomSet = RangeSet() + if i % 2 == 0 { + for _ in 0 ..< 10 { + let lower: Int64 = Int64.random(in: 0 ..< 4980736 + 131072) + let upper: Int64 = Int64.random(in: lower ..< (lower + 1234)) + randomSet.insert(contentsOf: lower ..< upper) + var ranges: [(Range, MediaBoxFetchPriority)] = [] + for range in randomSet.ranges { + ranges.append((range, .default)) + } + tempRanges.set(.single(ranges)) + } + } } - } - }) + + Thread.sleep(forTimeInterval: 0.1) + tempRanges.set(.single([])) + + Thread.sleep(forTimeInterval: 5.0) + tempRanges.set(.single([(0 ..< Int64.max, .default)])) + }).start() + + self.rangesDisposable = (tempRanges.get() + |> deliverOn(self.queue)).start(next: { [weak self] intervals in + if let strongSelf = self { + if let _ = strongSelf.currentIntervals { + strongSelf.currentIntervals = intervals + strongSelf.checkState() + } else { + strongSelf.currentIntervals = intervals + strongSelf.checkState() + } + } + }) + } else {*/ + self.rangesDisposable = (intervals + |> deliverOn(self.queue)).start(next: { [weak self] intervals in + if let strongSelf = self { + if let _ = strongSelf.currentIntervals { + strongSelf.currentIntervals = intervals + strongSelf.checkState() + } else { + strongSelf.currentIntervals = intervals + strongSelf.checkState() + } + } + }) + //} /*self.markSpeedRecord() self.speedTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in @@ -824,7 +868,20 @@ public func resourceFetchInfo(resource: TelegramMediaResource) -> MediaResourceF ) } -func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext?, resource: TelegramMediaResource, datacenterId: Int, size: Int64?, intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?, encryptionKey: SecretFileEncryptionKey? = nil, decryptedSize: Int64? = nil, continueInBackground: Bool = false, useMainConnection: Bool = false) -> Signal { +func multipartFetch( + postbox: Postbox, + network: Network, + mediaReferenceRevalidationContext: MediaReferenceRevalidationContext?, + resource: TelegramMediaResource, + datacenterId: Int, + size: Int64?, + intervals: Signal<[(Range, MediaBoxFetchPriority)], NoError>, + parameters: MediaResourceFetchParameters?, + encryptionKey: SecretFileEncryptionKey? = nil, + decryptedSize: Int64? = nil, + continueInBackground: Bool = false, + useMainConnection: Bool = false +) -> Signal { return Signal { subscriber in let location: MultipartFetchMasterLocation if let resource = resource as? MediaResourceWithWebFileReference { @@ -886,7 +943,7 @@ func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidati subscriber.putNext(.dataPart(resourceOffset: dataOffset, data: data, range: 0 ..< Int64(data.count), complete: false)) }, reportCompleteSize: { size in subscriber.putNext(.resourceSizeUpdated(size)) - subscriber.putCompletion() + //subscriber.putCompletion() }, finishWithError: { error in subscriber.putError(error) }, useMainConnection: useMainConnection) diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 33cd82d133b..53b79f1e65c 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -433,7 +433,7 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload case let .resource(resource): dataSignal = postbox.mediaBox.resourceData(resource.resource, option: .incremental(waitUntilFetchStatus: true)) |> map { MultipartUploadData.resourceData($0) } headerSize = resource.resource.headerSize - fetchedResource = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: resource) + fetchedResource = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource) |> map { _ in } case let .tempFile(file): if let size = fileSize(file.path) { diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 402d66d21df..c036f63a3c3 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -135,6 +135,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt return true case _ as SendAsMessageAttribute: return true + case _ as MediaSpoilerMessageAttribute: + return true default: return false } @@ -156,6 +158,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt return true case _ as SendAsMessageAttribute: return true + case _ as MediaSpoilerMessageAttribute: + return true case let attribute as ReplyMessageAttribute: if let forwardedMessageIds = forwardedMessageIds { return forwardedMessageIds.contains(attribute.messageId) diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 4fcbd889b73..714829923b2 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -423,6 +423,7 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans ttlSeconds = autoclearMessageAttribute.timeout } var stickers: [Api.InputDocument]? + var hasSpoiler = false for attribute in attributes { if let attribute = attribute as? EmbeddedMediaStickersMessageAttribute { var stickersValue: [Api.InputDocument] = [] @@ -435,7 +436,9 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans stickers = stickersValue flags |= 1 << 0 } - break + } else if let _ = attribute as? MediaSpoilerMessageAttribute { + flags |= 1 << 2 + hasSpoiler = true } } return postbox.transaction { transaction -> Api.InputPeer? in @@ -460,6 +463,9 @@ private func uploadedMediaImageContent(network: Network, postbox: Postbox, trans flags |= 1 << 0 ttlSeconds = autoclearMessageAttribute.timeout } + if hasSpoiler { + flags |= 1 << 1 + } return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil)), media: mediaImage) } default: @@ -726,6 +732,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if case let .done(file, thumbnail) = fileAndThumbnailResult { var flags: Int32 = 0 + var hasSpoiler = false var thumbnailFile: Api.InputFile? if case let .file(file) = thumbnail { thumbnailFile = file @@ -740,6 +747,9 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili if let attribute = attribute as? AutoclearTimeoutMessageAttribute { flags |= 1 << 1 ttlSeconds = attribute.timeout + } else if let _ = attribute as? MediaSpoilerMessageAttribute { + flags |= 1 << 5 + hasSpoiler = true } } @@ -788,7 +798,11 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili switch result { case let .messageMediaDocument(_, document, _): if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { - return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil)), media: mediaFile) + var flags: Int32 = 0 + if hasSpoiler { + flags |= (1 << 1) + } + return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil)), media: mediaFile) } default: break diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift index 6525f5e66d2..514cb632100 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneUploadedMedia.swift @@ -101,7 +101,7 @@ public func standaloneUploadedImage(account: Account, peerId: PeerId, text: Stri |> mapToSignal { result -> Signal in switch result { case let .encryptedFile(id, accessHash, size, dcId, _): - return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int64(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))))) + return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int64(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))))) case .encryptedFileEmpty: return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift b/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift index 0ecd9b9e9e4..e8aa7e0f0fa 100644 --- a/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/CacheStorageSettings.swift @@ -2,7 +2,6 @@ import Foundation import Postbox import SwiftSignalKit - public func updateCacheStorageSettingsInteractively(accountManager: AccountManager, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal { return accountManager.transaction { transaction -> Void in transaction.updateSharedData(SharedDataKeys.cacheStorageSettings, { entry in @@ -16,3 +15,17 @@ public func updateCacheStorageSettingsInteractively(accountManager: AccountManag }) } } + +public func updateAccountSpecificCacheStorageSettingsInteractively(postbox: Postbox, _ f: @escaping (AccountSpecificCacheStorageSettings) -> AccountSpecificCacheStorageSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.accountSpecificCacheStorageSettings, { entry in + let currentSettings: AccountSpecificCacheStorageSettings + if let entry = entry?.get(AccountSpecificCacheStorageSettings.self) { + currentSettings = entry + } else { + currentSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + return PreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 94c9d73ccc3..26c6eb9c861 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1103,7 +1103,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let messageText = text var medias: [Media] = [] - let (mediaValue, expirationTimer, nonPremium) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId) if let mediaValue = mediaValue { medias.append(mediaValue) } @@ -1115,6 +1115,10 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: attributes.append(NonPremiumMessageAttribute()) } + if let hasSpoiler = hasSpoiler, hasSpoiler { + attributes.append(MediaSpoilerMessageAttribute()) + } + if type.hasPrefix("auth") { updatedState.authorizationListUpdated = true } @@ -1301,14 +1305,6 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: return peer } }) - case let .updateUserPhoto(userId, _, photo, _): - updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), { peer in - if let user = peer as? TelegramUser { - return user.withUpdatedPhoto(parsedTelegramProfilePhoto(photo)) - } else { - return peer - } - }) case let .updateUserPhone(userId, phone): updatedState.updatePeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), { peer in if let user = peer as? TelegramUser { @@ -3457,7 +3453,7 @@ func replayFinalState( addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } deletedMessageIds.append(contentsOf: ids.map { .global($0) }) case let .DeleteMessages(ids): @@ -3474,7 +3470,7 @@ func replayFinalState( addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } case let .UpdatePeerChatInclusion(peerId, groupId, changedGroup): let currentInclusion = transaction.getPeerChatListInclusion(peerId) @@ -4250,7 +4246,7 @@ func replayFinalState( } updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration) case let .messageExtendedMedia(apiMedia): - let (media, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let media = media { updatedExtendedMedia = .full(media: media) } else { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 3b406e9fd5a..2fcbf53e45c 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -148,7 +148,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes text = updatedMessage.text forwardInfo = updatedMessage.forwardInfo } else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result { - let (mediaValue, _, nonPremium) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) + let (mediaValue, _, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId) if let mediaValue = mediaValue { media = [mediaValue] } else { @@ -176,6 +176,10 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedAttributes.append(NonPremiumMessageAttribute()) } + if let hasSpoiler = hasSpoiler, hasSpoiler { + updatedAttributes.append(MediaSpoilerMessageAttribute()) + } + if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud { for i in 0 ..< updatedAttributes.count { if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 7f88ae2e7f3..7476c2374f3 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -313,7 +313,7 @@ func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> for resource in resources { signals.append( - fetchedMediaResource(mediaBox: postbox.mediaBox, reference: .standalone(resource: resource)) + fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: resource)) |> ignoreValues |> `catch` { _ -> Signal in return .complete() diff --git a/submodules/TelegramCore/Sources/State/Fetch.swift b/submodules/TelegramCore/Sources/State/Fetch.swift index b6bced6a66f..5e65c431971 100644 --- a/submodules/TelegramCore/Sources/State/Fetch.swift +++ b/submodules/TelegramCore/Sources/State/Fetch.swift @@ -75,7 +75,13 @@ func fetchResource(account: Account, resource: MediaResource, intervals: Signal< return .fail(.generic) } return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false)) - |> then(fetchCloudMediaLocation(account: account, resource: cloudResource, datacenterId: cloudResource.datacenterId, size: resource.size == 0 ? nil : resource.size, intervals: intervals, parameters: MediaResourceFetchParameters(tag: nil, info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), isRandomAccessAllowed: true))) + |> then(fetchCloudMediaLocation(account: account, resource: cloudResource, datacenterId: cloudResource.datacenterId, size: resource.size == 0 ? nil : resource.size, intervals: intervals, parameters: MediaResourceFetchParameters( + tag: nil, + info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), + location: nil, + contentType: .other, + isRandomAccessAllowed: true + ))) } } return nil diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift index 7601c76dae4..a8cdca1fdf2 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift @@ -240,6 +240,8 @@ private func installRemoteStickerPacks(network: Network, infos: [StickerPackColl archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) case let .stickerSetFullCovered(set, _, _, _): archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetNoCovered(set): + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) } } return archivedIds diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 686b2ee8978..bc3781c3a7c 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -784,10 +784,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } @@ -810,7 +810,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -825,7 +825,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -843,7 +843,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -853,7 +853,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -986,10 +986,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } @@ -1013,7 +1013,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1044,7 +1044,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1062,7 +1062,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -1072,7 +1072,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -1265,10 +1265,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } @@ -1292,7 +1292,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1323,7 +1323,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1341,7 +1341,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -1351,7 +1351,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -1466,10 +1466,10 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } @@ -1493,7 +1493,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1524,7 +1524,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes) @@ -1542,7 +1542,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } @@ -1552,7 +1552,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index babdb592709..279a17b8c86 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 150 + return 151 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift index 2f8cd3b50ad..2eb0d1d49bb 100644 --- a/submodules/TelegramCore/Sources/State/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -2,6 +2,7 @@ import Foundation import TelegramApi import Postbox import SwiftSignalKit +import MtProtoKit enum FeaturedStickerPacksCategory { case stickerPacks @@ -48,6 +49,52 @@ func manageStickerPacks(network: Network, postbox: Postbox) -> Signal then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } +func resolveMissingStickerSets(network: Network, postbox: Postbox, stickerSets: [Api.StickerSetCovered], ignorePacksWithHashes: [Int64: Int32]) -> Signal<[Api.StickerSetCovered], NoError> { + var missingSignals: [Signal<(Int, Api.StickerSetCovered)?, NoError>] = [] + for i in 0 ..< stickerSets.count { + switch stickerSets[i] { + case let .stickerSetNoCovered(value): + switch value { + case let .stickerSet(_, _, id, accessHash, _, _, _, _, _, _, _, hash): + if ignorePacksWithHashes[id] == hash { + continue + } + + missingSignals.append(network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: id, accessHash: accessHash), hash: 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> (Int, Api.StickerSetCovered)? in + if let result = result { + switch result { + case let .stickerSet(set, packs, keywords, documents): + return (i, Api.StickerSetCovered.stickerSetFullCovered(set: set, packs: packs, keywords: keywords, documents: documents)) + case .stickerSetNotModified: + return nil + } + } else { + return nil + } + }) + } + default: + break + } + } + + return combineLatest(missingSignals) + |> map { results -> [Api.StickerSetCovered] in + var updatedSets = stickerSets + for result in results { + if let result = result { + updatedSets[result.0] = result.1 + } + } + return updatedSets + } +} + func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: FeaturedStickerPacksCategory) -> Signal { return postbox.transaction { transaction -> Signal in let initialPacks = transaction.getOrderedListItems(collectionId: category.itemListNamespace) @@ -75,28 +122,34 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: F switch category { case .stickerPacks: signal = network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash)) - |> map { result -> FeaturedList in + |> mapToSignal { result -> Signal in switch result { case .featuredStickersNotModified: - return .notModified + return .single(.notModified) case let .featuredStickers(flags, _, _, sets, unread): - let unreadIds = Set(unread) - var updatedPacks: [FeaturedStickerPackItem] = [] - for set in sets { - var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) - if let previousPack = initialPackMap[info.id.id] { - if previousPack.info.hash == info.hash { - items = previousPack.topItems + return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.mapValues({ item in + item.info.hash + })) + |> castError(MTRpcError.self) + |> map { sets -> FeaturedList in + let unreadIds = Set(unread) + var updatedPacks: [FeaturedStickerPackItem] = [] + for set in sets { + var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) + if let previousPack = initialPackMap[info.id.id] { + if previousPack.info.hash == info.hash { + items = previousPack.topItems + } } + updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) } - updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) + let isPremium = flags & (1 << 0) != 0 + return .content(FeaturedListContent( + unreadIds: unreadIds, + packs: updatedPacks, + isPremium: isPremium + )) } - let isPremium = flags & (1 << 0) != 0 - return .content(FeaturedListContent( - unreadIds: unreadIds, - packs: updatedPacks, - isPremium: isPremium - )) } } |> `catch` { _ -> Signal in @@ -104,28 +157,34 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: F } case .emojiPacks: signal = network.request(Api.functions.messages.getFeaturedEmojiStickers(hash: initialHash)) - |> map { result -> FeaturedList in + |> mapToSignal { result -> Signal in switch result { case .featuredStickersNotModified: - return .notModified + return .single(.notModified) case let .featuredStickers(flags, _, _, sets, unread): - let unreadIds = Set(unread) - var updatedPacks: [FeaturedStickerPackItem] = [] - for set in sets { - var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) - if let previousPack = initialPackMap[info.id.id] { - if previousPack.info.hash == info.hash { - items = previousPack.topItems + return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.mapValues({ item in + item.info.hash + })) + |> castError(MTRpcError.self) + |> map { sets -> FeaturedList in + let unreadIds = Set(unread) + var updatedPacks: [FeaturedStickerPackItem] = [] + for set in sets { + var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) + if let previousPack = initialPackMap[info.id.id] { + if previousPack.info.hash == info.hash { + items = previousPack.topItems + } } + updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) } - updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) + let isPremium = flags & (1 << 0) != 0 + return .content(FeaturedListContent( + unreadIds: unreadIds, + packs: updatedPacks, + isPremium: isPremium + )) } - let isPremium = flags & (1 << 0) != 0 - return .content(FeaturedListContent( - unreadIds: unreadIds, - packs: updatedPacks, - isPremium: isPremium - )) } } |> `catch` { _ -> Signal in @@ -273,5 +332,9 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect } } return (info, items) + case let .stickerSetNoCovered(set): + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + let items: [StickerPackItem] = [] + return (info, items) } } diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index b7c342eb97a..acbb3567b6f 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -301,8 +301,6 @@ extension Api.Update { return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] case let .updateUserPhone(userId, _): return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] - case let .updateUserPhoto(userId, _, _, _): - return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] case let .updateServiceNotification(_, inboxDate, _, _, _, _): if let _ = inboxDate { return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000))] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift index 01c68345ee4..51d634ef59a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CacheStorageSettings.swift @@ -1,17 +1,44 @@ +import Foundation import Postbox public struct CacheStorageSettings: Codable, Equatable { - public let defaultCacheStorageTimeout: Int32 - public let defaultCacheStorageLimitGigabytes: Int32 + public enum PeerStorageCategory: String, Codable, Hashable { + case privateChats = "privateChats" + case groups = "groups" + case channels = "channels" + } + + private struct CategoryStorageTimeoutRepresentation: Codable { + var key: PeerStorageCategory + var value: Int32 + } + + public var defaultCacheStorageTimeout: Int32 + public var defaultCacheStorageLimitGigabytes: Int32 + + public var categoryStorageTimeout: [PeerStorageCategory: Int32] public static var defaultSettings: CacheStorageSettings { - // MARK: Nicegram CacheSettings, change default values - return CacheStorageSettings(defaultCacheStorageTimeout: 3 * 24 * 60 * 60, defaultCacheStorageLimitGigabytes: 2) + // MARK: Nicegram CacheSettings, change defaultCacheStorageLimitGigabytes to 5 * 1024 * 1024 + return CacheStorageSettings( + defaultCacheStorageTimeout: Int32.max, + defaultCacheStorageLimitGigabytes: 5 * 1024 * 1024, + categoryStorageTimeout: [ + .privateChats: Int32.max, + .groups: Int32(31 * 24 * 60 * 60), + .channels: Int32(31 * 24 * 60 * 60) + ] + ) } - public init(defaultCacheStorageTimeout: Int32, defaultCacheStorageLimitGigabytes: Int32) { + public init( + defaultCacheStorageTimeout: Int32, + defaultCacheStorageLimitGigabytes: Int32, + categoryStorageTimeout: [PeerStorageCategory: Int32] + ) { self.defaultCacheStorageTimeout = defaultCacheStorageTimeout self.defaultCacheStorageLimitGigabytes = defaultCacheStorageLimitGigabytes + self.categoryStorageTimeout = categoryStorageTimeout } public init(from decoder: Decoder) throws { @@ -24,8 +51,22 @@ public struct CacheStorageSettings: Codable, Equatable { } else if let value = try container.decodeIfPresent(Int32.self, forKey: "sizeLimit") { self.defaultCacheStorageLimitGigabytes = value } else { - // MARK: Nicegram CacheSettings, change default value to 2 GB - self.defaultCacheStorageLimitGigabytes = 2 * 1024 * 1024 + // MARK: Nicegram CacheSettings, change defaultCacheStorageLimitGigabytes to 5 * 1024 * 1024 + self.defaultCacheStorageLimitGigabytes = 5 * 1024 * 1024 + } + + if let data = try container.decodeIfPresent(Data.self, forKey: "categoryStorageTimeoutJson") { + if let items = try? JSONDecoder().decode([CategoryStorageTimeoutRepresentation].self, from: data) { + var categoryStorageTimeout: [PeerStorageCategory: Int32] = [:] + for item in items { + categoryStorageTimeout[item.key] = item.value + } + self.categoryStorageTimeout = categoryStorageTimeout + } else { + self.categoryStorageTimeout = CacheStorageSettings.defaultSettings.categoryStorageTimeout + } + } else { + self.categoryStorageTimeout = CacheStorageSettings.defaultSettings.categoryStorageTimeout } } @@ -34,12 +75,73 @@ public struct CacheStorageSettings: Codable, Equatable { try container.encode(self.defaultCacheStorageTimeout, forKey: "dt") try container.encode(self.defaultCacheStorageLimitGigabytes, forKey: "dl") + + var categoryStorageTimeoutValues: [CategoryStorageTimeoutRepresentation] = [] + for (key, value) in self.categoryStorageTimeout { + categoryStorageTimeoutValues.append(CategoryStorageTimeoutRepresentation(key: key, value: value)) + } + if let data = try? JSONEncoder().encode(categoryStorageTimeoutValues) { + try container.encode(data, forKey: "categoryStorageTimeoutJson") + } + } +} + +public struct AccountSpecificCacheStorageSettings: Codable, Equatable { + private struct PeerStorageTimeoutExceptionRepresentation: Codable { + var key: PeerId + var value: Int32 } - public func withUpdatedDefaultCacheStorageTimeout(_ defaultCacheStorageTimeout: Int32) -> CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: self.defaultCacheStorageLimitGigabytes) + public struct Value : Equatable { + public let key: PeerId + public let value: Int32 + public init(key: PeerId, value: Int32) { + self.key = key + self.value = value + } } - public func withUpdatedDefaultCacheStorageLimitGigabytes(_ defaultCacheStorageLimitGigabytes: Int32) -> CacheStorageSettings { - return CacheStorageSettings(defaultCacheStorageTimeout: self.defaultCacheStorageTimeout, defaultCacheStorageLimitGigabytes: defaultCacheStorageLimitGigabytes) + + public var peerStorageTimeoutExceptions: [Value] + + public static var defaultSettings: AccountSpecificCacheStorageSettings { + return AccountSpecificCacheStorageSettings( + peerStorageTimeoutExceptions: [] + ) + } + + public init( + peerStorageTimeoutExceptions: [Value] + ) { + self.peerStorageTimeoutExceptions = peerStorageTimeoutExceptions + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let data = try container.decodeIfPresent(Data.self, forKey: "peerStorageTimeoutExceptionsJson") { + if let items = try? JSONDecoder().decode([PeerStorageTimeoutExceptionRepresentation].self, from: data) { + var peerStorageTimeoutExceptions: [Value] = [] + for item in items { + peerStorageTimeoutExceptions.append(.init(key: item.key, value: item.value)) + } + self.peerStorageTimeoutExceptions = peerStorageTimeoutExceptions + } else { + self.peerStorageTimeoutExceptions = AccountSpecificCacheStorageSettings.defaultSettings.peerStorageTimeoutExceptions + } + } else { + self.peerStorageTimeoutExceptions = AccountSpecificCacheStorageSettings.defaultSettings.peerStorageTimeoutExceptions + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + var peerStorageTimeoutExceptionsValues: [PeerStorageTimeoutExceptionRepresentation] = [] + for value in self.peerStorageTimeoutExceptions { + peerStorageTimeoutExceptionsValues.append(PeerStorageTimeoutExceptionRepresentation(key: value.key, value: value.value)) + } + if let data = try? JSONEncoder().encode(peerStorageTimeoutExceptionsValues) { + try container.encode(data, forKey: "peerStorageTimeoutExceptionsJson") + } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index b06ba78e42f..74c5dc1432d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -151,6 +151,14 @@ public struct PeerGeoLocation: PostboxCoding, Equatable { } } +public struct PeerMembersHidden: Codable, Equatable { + public var value: Bool + + public init(value: Bool) { + self.value = value + } +} + public final class CachedChannelData: CachedPeerData { public enum LinkedDiscussionPeerId: Equatable { case unknown @@ -240,6 +248,7 @@ public final class CachedChannelData: CachedPeerData { public let inviteRequestsPending: Int32? public let sendAsPeerId: PeerId? public let allowedReactions: EnginePeerCachedInfoItem + public let membersHidden: EnginePeerCachedInfoItem public let peerIds: Set public let messageIds: Set @@ -278,6 +287,7 @@ public final class CachedChannelData: CachedPeerData { self.inviteRequestsPending = nil self.sendAsPeerId = nil self.allowedReactions = .unknown + self.membersHidden = .unknown } public init( @@ -308,7 +318,8 @@ public final class CachedChannelData: CachedPeerData { themeEmoticon: String?, inviteRequestsPending: Int32?, sendAsPeerId: PeerId?, - allowedReactions: EnginePeerCachedInfoItem + allowedReactions: EnginePeerCachedInfoItem, + membersHidden: EnginePeerCachedInfoItem ) { self.isNotAccessible = isNotAccessible self.flags = flags @@ -338,6 +349,7 @@ public final class CachedChannelData: CachedPeerData { self.inviteRequestsPending = inviteRequestsPending self.sendAsPeerId = sendAsPeerId self.allowedReactions = allowedReactions + self.membersHidden = membersHidden var peerIds = Set() for botInfo in botInfos { @@ -365,115 +377,119 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: LinkedDiscussionPeerId) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedInvitedOn(_ invitedOn: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedActiveCall(_ activeCall: ActiveCall?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedPendingSuggestions(_ pendingSuggestions: [String]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedSendAsPeerId(_ sendAsPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden) } public func withUpdatedAllowedReactions(_ allowedReactions: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions, membersHidden: self.membersHidden) + } + + public func withUpdatedMembersHidden(_ membersHidden: EnginePeerCachedInfoItem) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: membersHidden) } public init(decoder: PostboxDecoder) { @@ -571,6 +587,12 @@ public final class CachedChannelData: CachedPeerData { self.allowedReactions = .unknown } + if let membersHidden = decoder.decode(PeerMembersHidden.self, forKey: "membersHidden") { + self.membersHidden = .known(membersHidden) + } else { + self.membersHidden = .unknown + } + if case let .known(linkedDiscussionPeerIdValue) = self.linkedDiscussionPeerId { if let linkedDiscussionPeerIdValue = linkedDiscussionPeerIdValue { peerIds.insert(linkedDiscussionPeerIdValue) @@ -721,6 +743,13 @@ public final class CachedChannelData: CachedPeerData { case let .known(value): encoder.encode(value, forKey: "allowedReactionSet") } + + switch self.membersHidden { + case .unknown: + encoder.encodeNil(forKey: "membersHidden") + case let .known(value): + encoder.encode(value, forKey: "membersHidden") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -840,6 +869,10 @@ public final class CachedChannelData: CachedPeerData { return false } + if other.membersHidden != self.membersHidden { + return false + } + return true } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 49120073d07..4cba49c15f5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -152,6 +152,8 @@ public final class CachedUserData: CachedPeerData { public let autoremoveTimeout: CachedPeerAutoremoveTimeout public let themeEmoticon: String? public let photo: CachedPeerProfilePhoto + public let personalPhoto: CachedPeerProfilePhoto + public let fallbackPhoto: CachedPeerProfilePhoto public let premiumGiftOptions: [CachedPremiumGiftOption] public let voiceMessagesAvailable: Bool @@ -174,13 +176,15 @@ public final class CachedUserData: CachedPeerData { self.autoremoveTimeout = .unknown self.themeEmoticon = nil self.photo = .unknown + self.personalPhoto = .unknown + self.fallbackPhoto = .unknown self.premiumGiftOptions = [] self.voiceMessagesAvailable = true self.peerIds = Set() self.messageIds = Set() } - public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool) { + public init(about: String?, botInfo: BotInfo?, peerStatusSettings: PeerStatusSettings?, pinnedMessageId: MessageId?, isBlocked: Bool, commonGroupCount: Int32, voiceCallsAvailable: Bool, videoCallsAvailable: Bool, callsPrivate: Bool, canPinMessages: Bool, hasScheduledMessages: Bool, autoremoveTimeout: CachedPeerAutoremoveTimeout, themeEmoticon: String?, photo: CachedPeerProfilePhoto, personalPhoto: CachedPeerProfilePhoto, fallbackPhoto: CachedPeerProfilePhoto, premiumGiftOptions: [CachedPremiumGiftOption], voiceMessagesAvailable: Bool) { self.about = about self.botInfo = botInfo self.peerStatusSettings = peerStatusSettings @@ -195,6 +199,8 @@ public final class CachedUserData: CachedPeerData { self.autoremoveTimeout = autoremoveTimeout self.themeEmoticon = themeEmoticon self.photo = photo + self.personalPhoto = personalPhoto + self.fallbackPhoto = fallbackPhoto self.premiumGiftOptions = premiumGiftOptions self.voiceMessagesAvailable = voiceMessagesAvailable @@ -233,6 +239,8 @@ public final class CachedUserData: CachedPeerData { self.themeEmoticon = decoder.decodeOptionalStringForKey("te") self.photo = decoder.decodeObjectForKey("phv", decoder: CachedPeerProfilePhoto.init(decoder:)) as? CachedPeerProfilePhoto ?? .unknown + self.personalPhoto = decoder.decodeObjectForKey("pphv", decoder: CachedPeerProfilePhoto.init(decoder:)) as? CachedPeerProfilePhoto ?? .unknown + self.fallbackPhoto = decoder.decodeObjectForKey("fphv", decoder: CachedPeerProfilePhoto.init(decoder:)) as? CachedPeerProfilePhoto ?? .unknown self.premiumGiftOptions = decoder.decodeObjectArrayWithDecoderForKey("pgo") as [CachedPremiumGiftOption] self.voiceMessagesAvailable = decoder.decodeInt32ForKey("vma", orElse: 0) != 0 @@ -286,6 +294,8 @@ public final class CachedUserData: CachedPeerData { } encoder.encodeObject(self.photo, forKey: "phv") + encoder.encodeObject(self.personalPhoto, forKey: "pphv") + encoder.encodeObject(self.fallbackPhoto, forKey: "fphv") encoder.encodeObjectArray(self.premiumGiftOptions, forKey: "pgo") encoder.encodeInt32(self.voiceMessagesAvailable ? 1 : 0, forKey: "vma") @@ -303,70 +313,78 @@ public final class CachedUserData: CachedPeerData { return false } - return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable + return other.about == self.about && other.botInfo == self.botInfo && self.peerStatusSettings == other.peerStatusSettings && self.isBlocked == other.isBlocked && self.commonGroupCount == other.commonGroupCount && self.voiceCallsAvailable == other.voiceCallsAvailable && self.videoCallsAvailable == other.videoCallsAvailable && self.callsPrivate == other.callsPrivate && self.hasScheduledMessages == other.hasScheduledMessages && self.autoremoveTimeout == other.autoremoveTimeout && self.themeEmoticon == other.themeEmoticon && self.photo == other.photo && self.personalPhoto == other.personalPhoto && self.fallbackPhoto == other.fallbackPhoto && self.premiumGiftOptions == other.premiumGiftOptions && self.voiceMessagesAvailable == other.voiceMessagesAvailable } public func withUpdatedAbout(_ about: String?) -> CachedUserData { - return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedBotInfo(_ botInfo: BotInfo?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedIsBlocked(_ isBlocked: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedCommonGroupCount(_ commonGroupCount: Int32) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedVoiceCallsAvailable(_ voiceCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedVideoCallsAvailable(_ videoCallsAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedCallsPrivate(_ callsPrivate: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedCanPinMessages(_ canPinMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedPhoto(_ photo: CachedPeerProfilePhoto) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + } + + public func withUpdatedPersonalPhoto(_ personalPhoto: CachedPeerProfilePhoto) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + } + + public func withUpdatedFallbackPhoto(_ fallbackPhoto: CachedPeerProfilePhoto) -> CachedUserData { + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: premiumGiftOptions, voiceMessagesAvailable: self.voiceMessagesAvailable) } public func withUpdatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> CachedUserData { - return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable) + return CachedUserData(about: self.about, botInfo: self.botInfo, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, isBlocked: self.isBlocked, commonGroupCount: self.commonGroupCount, voiceCallsAvailable: self.voiceCallsAvailable, videoCallsAvailable: self.videoCallsAvailable, callsPrivate: self.callsPrivate, canPinMessages: self.canPinMessages, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, themeEmoticon: self.themeEmoticon, photo: self.photo, personalPhoto: self.personalPhoto, fallbackPhoto: self.fallbackPhoto, premiumGiftOptions: self.premiumGiftOptions, voiceMessagesAvailable: voiceMessagesAvailable) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaSpoilerMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaSpoilerMessageAttribute.swift new file mode 100644 index 00000000000..c01d689fdad --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaSpoilerMessageAttribute.swift @@ -0,0 +1,16 @@ +import Foundation +import Postbox + +public class MediaSpoilerMessageAttribute: MessageAttribute { + public var associatedMessageIds: [MessageId] = [] + + public init() { + + } + + required public init(decoder: PostboxDecoder) { + } + + public func encode(_ encoder: PostboxEncoder) { + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 9e2533826b4..95d00722175 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -247,6 +247,7 @@ private enum PreferencesKeyValues: Int32 { case reactionSettings = 24 case premiumPromo = 26 case globalMessageAutoremoveTimeoutSettings = 27 + case accountSpecificCacheStorageSettings = 28 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -381,6 +382,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.globalMessageAutoremoveTimeoutSettings.rawValue) return key }() + + public static let accountSpecificCacheStorageSettings: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.accountSpecificCacheStorageSettings.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift index 4c45bab232e..7ee7a138823 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift @@ -158,6 +158,7 @@ public struct ReplyMarkupMessageFlags: OptionSet { public static let setupReply = ReplyMarkupMessageFlags(rawValue: 1 << 2) public static let inline = ReplyMarkupMessageFlags(rawValue: 1 << 3) public static let fit = ReplyMarkupMessageFlags(rawValue: 1 << 4) + public static let persistent = ReplyMarkupMessageFlags(rawValue: 1 << 5) } public class ReplyMarkupMessageAttribute: MessageAttribute, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 66203a036c2..88e73af9e43 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -98,6 +98,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giftPremium(currency: String, amount: Int64, months: Int32) case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?) case topicEdited(components: [ForumTopicEditComponent]) + case suggestedProfilePhoto(image: TelegramMediaImage?) + case attachMenuBotAllowed public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -172,6 +174,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId")) case 29: self = .topicEdited(components: decoder.decodeObjectArrayWithDecoderForKey("components")) + case 30: + self = .suggestedProfilePhoto(image: decoder.decodeObjectForKey("image") as? TelegramMediaImage) + case 31: + self = .attachMenuBotAllowed default: self = .unknown } @@ -318,6 +324,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .topicEdited(components): encoder.encodeInt32(29, forKey: "_rawValue") encoder.encodeObjectArray(components, forKey: "components") + case let .suggestedProfilePhoto(image): + encoder.encodeInt32(30, forKey: "_rawValue") + if let image = image { + encoder.encodeObject(image, forKey: "image") + } + case .attachMenuBotAllowed: + encoder.encodeInt32(31, forKey: "_rawValue") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index ca354567513..3f831c4da78 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -190,7 +190,7 @@ public struct TelegramMediaVideoFlags: OptionSet { public static let supportsStreaming = TelegramMediaVideoFlags(rawValue: 1 << 1) } -public struct StickerMaskCoords: PostboxCoding { +public struct StickerMaskCoords: PostboxCoding, Equatable { public let n: Int32 public let x: Double public let y: Double @@ -218,7 +218,7 @@ public struct StickerMaskCoords: PostboxCoding { } } -public enum TelegramMediaFileAttribute: PostboxCoding { +public enum TelegramMediaFileAttribute: PostboxCoding, Equatable { case FileName(fileName: String) case Sticker(displayText: String, packReference: StickerPackReference?, maskData: StickerMaskCoords?) case ImageSize(size: PixelDimensions) @@ -229,7 +229,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding { case hintFileIsLarge case hintIsValidated case NoPremium - case CustomEmoji(isPremium: Bool, alt: String, packReference: StickerPackReference?) + case CustomEmoji(isPremium: Bool, isSingleColor: Bool, alt: String, packReference: StickerPackReference?) public init(decoder: PostboxDecoder) { let type: Int32 = decoder.decodeInt32ForKey("t", orElse: 0) @@ -260,7 +260,7 @@ public enum TelegramMediaFileAttribute: PostboxCoding { case typeNoPremium: self = .NoPremium case typeCustomEmoji: - self = .CustomEmoji(isPremium: decoder.decodeBoolForKey("ip", orElse: true), alt: decoder.decodeStringForKey("dt", orElse: ""), packReference: decoder.decodeObjectForKey("pr", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference) + self = .CustomEmoji(isPremium: decoder.decodeBoolForKey("ip", orElse: true), isSingleColor: decoder.decodeBoolForKey("sc", orElse: false), alt: decoder.decodeStringForKey("dt", orElse: ""), packReference: decoder.decodeObjectForKey("pr", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference) default: preconditionFailure() } @@ -317,9 +317,10 @@ public enum TelegramMediaFileAttribute: PostboxCoding { encoder.encodeInt32(typeHintIsValidated, forKey: "t") case .NoPremium: encoder.encodeInt32(typeNoPremium, forKey: "t") - case let .CustomEmoji(isPremium, alt, packReference): + case let .CustomEmoji(isPremium, isSingleColor, alt, packReference): encoder.encodeInt32(typeCustomEmoji, forKey: "t") encoder.encodeBool(isPremium, forKey: "ip") + encoder.encodeBool(isSingleColor, forKey: "sc") encoder.encodeString(alt, forKey: "dt") if let packReference = packReference { encoder.encodeObject(packReference, forKey: "pr") @@ -652,7 +653,7 @@ public final class TelegramMediaFile: Media, Equatable, Codable { public var isPremiumEmoji: Bool { for attribute in self.attributes { - if case let .CustomEmoji(isPremium, _, _) = attribute { + if case let .CustomEmoji(isPremium, _, _, _) = attribute { return isPremium } } @@ -737,6 +738,10 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + if self.attributes != other.attributes { + return false + } + return true } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaImage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaImage.swift index c49dd9754f4..c4c1c85ff96 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaImage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaImage.swift @@ -306,12 +306,15 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C public let progressiveSizes: [Int32] public let immediateThumbnailData: Data? public let hasVideo: Bool - public init(dimensions: PixelDimensions, resource: TelegramMediaResource, progressiveSizes: [Int32], immediateThumbnailData: Data?, hasVideo: Bool) { + public let isPersonal: Bool + + public init(dimensions: PixelDimensions, resource: TelegramMediaResource, progressiveSizes: [Int32], immediateThumbnailData: Data?, hasVideo: Bool, isPersonal: Bool) { self.dimensions = dimensions self.resource = resource self.progressiveSizes = progressiveSizes self.immediateThumbnailData = immediateThumbnailData self.hasVideo = hasVideo + self.isPersonal = isPersonal } public init(decoder: PostboxDecoder) { @@ -320,6 +323,7 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C self.progressiveSizes = decoder.decodeInt32ArrayForKey("ps") self.immediateThumbnailData = decoder.decodeDataForKey("th") self.hasVideo = decoder.decodeBoolForKey("hv", orElse: false) + self.isPersonal = decoder.decodeBoolForKey("ip", orElse: false) } public func encode(_ encoder: PostboxEncoder) { @@ -333,6 +337,7 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C encoder.encodeNil(forKey: "th") } encoder.encodeBool(self.hasVideo, forKey: "hv") + encoder.encodeBool(self.isPersonal, forKey: "ip") } public var description: String { @@ -355,6 +360,9 @@ public final class TelegramMediaImageRepresentation: PostboxCoding, Equatable, C if self.hasVideo != other.hasVideo { return false } + if self.isPersonal != other.isPersonal { + return false + } return true } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index f2247272088..5a1b3fa5368 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -44,7 +44,7 @@ public extension TelegramEngine { } public func updateAccountPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, fallback: false, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public func updatePeerPhotoExisting(reference: TelegramMediaImageReference) -> Signal { @@ -52,7 +52,15 @@ public extension TelegramEngine { } public func removeAccountPhoto(reference: TelegramMediaImageReference?) -> Signal { - return _internal_removeAccountPhoto(network: self.account.network, reference: reference) + return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: false) + } + + public func updateFallbackPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, fallback: true, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + } + + public func removeFallbackPhoto(reference: TelegramMediaImageReference?) -> Signal { + return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: true) } public func setEmojiStatus(file: TelegramMediaFile?, expirationDate: Int32?) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index 486fc79d590..714aa374624 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -1,3 +1,4 @@ +import Foundation import SwiftSignalKit import Postbox @@ -25,6 +26,10 @@ public extension TelegramEngine { return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName) } + public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + } + public func deviceContactsImportedByCount(contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> { return _internal_deviceContactsImportedByCount(postbox: self.account.postbox, contacts: contacts) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index cfd6ff6e333..17316edc484 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -11,6 +11,7 @@ public final class AttachMenuBots: Equatable, Codable { case botIcons case peerTypes case hasSettings + case flags } public enum IconName: Int32, Codable { @@ -38,6 +39,21 @@ public final class AttachMenuBots: Equatable, Codable { } } + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let hasSettings = Flags(rawValue: 1 << 0) + public static let requiresWriteAccess = Flags(rawValue: 1 << 1) + } + public struct PeerFlags: OptionSet, Codable { public var rawValue: UInt32 @@ -94,20 +110,20 @@ public final class AttachMenuBots: Equatable, Codable { public let name: String public let icons: [IconName: TelegramMediaFile] public let peerTypes: PeerFlags - public let hasSettings: Bool + public let flags: Flags public init( peerId: PeerId, name: String, icons: [IconName: TelegramMediaFile], peerTypes: PeerFlags, - hasSettings: Bool + flags: Flags ) { self.peerId = peerId self.name = name self.icons = icons self.peerTypes = peerTypes - self.hasSettings = hasSettings + self.flags = flags } public static func ==(lhs: Bot, rhs: Bot) -> Bool { @@ -123,7 +139,7 @@ public final class AttachMenuBots: Equatable, Codable { if lhs.peerTypes != rhs.peerTypes { return false } - if lhs.hasSettings != rhs.hasSettings { + if lhs.flags != rhs.flags { return false } return true @@ -147,7 +163,12 @@ public final class AttachMenuBots: Equatable, Codable { let value = try container.decodeIfPresent(Int32.self, forKey: .peerTypes) ?? Int32(PeerFlags.default.rawValue) self.peerTypes = PeerFlags(rawValue: UInt32(value)) - self.hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false + if let flags = try container.decodeIfPresent(Int32.self, forKey: .flags) { + self.flags = Flags(rawValue: flags) + } else { + let hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false + self.flags = hasSettings ? [.hasSettings] : [] + } } public func encode(to encoder: Encoder) throws { @@ -164,7 +185,7 @@ public final class AttachMenuBots: Equatable, Codable { try container.encode(Int32(self.peerTypes.rawValue), forKey: .peerTypes) - try container.encode(self.hasSettings, forKey: .hasSettings) + try container.encode(Int32(self.flags.rawValue), forKey: .flags) } } @@ -276,7 +297,7 @@ func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force: var resultBots: [AttachMenuBots.Bot] = [] for bot in bots { switch bot { - case let .attachMenuBot(flags, botId, name, apiPeerTypes, botIcons): + case let .attachMenuBot(apiFlags, botId, name, apiPeerTypes, botIcons): var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:] for icon in botIcons { switch icon { @@ -302,7 +323,14 @@ func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force: peerTypes.insert(.channel) } } - resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, hasSettings: (flags & (1 << 1)) != 0)) + var flags: AttachMenuBots.Bot.Flags = [] + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.hasSettings) + } + if (apiFlags & (1 << 2)) != 0 { + flags.insert(.requiresWriteAccess) + } + resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } } @@ -340,12 +368,16 @@ public enum AddBotToAttachMenuError { } -func _internal_addBotToAttachMenu(postbox: Postbox, network: Network, botId: PeerId) -> Signal { +func _internal_addBotToAttachMenu(postbox: Postbox, network: Network, botId: PeerId, allowWrite: Bool) -> Signal { return postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else { return .complete() } - return network.request(Api.functions.messages.toggleBotInAttachMenu(bot: inputUser, enabled: .boolTrue)) + var flags: Int32 = 0 + if allowWrite { + flags |= (1 << 0) + } + return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: flags, bot: inputUser, enabled: .boolTrue)) |> map { value -> Bool in switch value { case .boolTrue: @@ -379,7 +411,7 @@ func _internal_removeBotFromAttachMenu(postbox: Postbox, network: Network, botId guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else { return .complete() } - return network.request(Api.functions.messages.toggleBotInAttachMenu(bot: inputUser, enabled: .boolFalse)) + return network.request(Api.functions.messages.toggleBotInAttachMenu(flags: 0, bot: inputUser, enabled: .boolFalse)) |> map { value -> Bool in switch value { case .boolTrue: @@ -406,14 +438,14 @@ public struct AttachMenuBot { public let shortName: String public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] public let peerTypes: AttachMenuBots.Bot.PeerFlags - public let hasSettings: Bool + public let flags: AttachMenuBots.Bot.Flags - init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, hasSettings: Bool) { + init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { self.peer = peer self.shortName = shortName self.icons = icons self.peerTypes = peerTypes - self.hasSettings = hasSettings + self.flags = flags } } @@ -425,7 +457,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr var resultBots: [AttachMenuBot] = [] for bot in cachedBots { if let peer = transaction.getPeer(bot.peerId) { - resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, hasSettings: bot.hasSettings)) + resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } return resultBots @@ -440,7 +472,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId return postbox.transaction { transaction -> Signal in if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots { if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) { - return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, hasSettings: bot.hasSettings)) + return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } @@ -474,7 +506,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId } switch bot { - case let .attachMenuBot(flags, _, name, apiPeerTypes, botIcons): + case let .attachMenuBot(apiFlags, _, name, apiPeerTypes, botIcons): var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:] for icon in botIcons { switch icon { @@ -499,7 +531,14 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId peerTypes.insert(.channel) } } - return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, hasSettings: (flags & (1 << 1)) != 0)) + var flags: AttachMenuBots.Bot.Flags = [] + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.hasSettings) + } + if (apiFlags & (1 << 2)) != 0 { + flags.insert(.requiresWriteAccess) + } + return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 3a5ba967112..4c8c2638ee8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -34,7 +34,7 @@ public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBo } } if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } for id in ids { if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud { @@ -62,7 +62,7 @@ func _internal_deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: M addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds))).start() } } @@ -72,7 +72,7 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } } @@ -84,7 +84,7 @@ func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId return true }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } } transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in @@ -101,7 +101,7 @@ func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, return true }) if !resourceIds.isEmpty { - let _ = mediaBox.removeCachedResources(Set(resourceIds), force: true).start() + let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start() } } transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift index da8df52333a..c3174d50ab0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/OutgoingMessageWithChatContextResult.swift @@ -69,7 +69,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: arc4random_buf(&randomId, 8) let thumbnailResource = thumbnail.resource let imageDimensions = thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: tmpImage), replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) } else { return .message(text: caption, attributes: attributes, inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: []) @@ -85,7 +85,7 @@ func _internal_outgoingMessageWithChatContextResult(to peerId: PeerId, threadId: if thumbnail.mimeType.hasPrefix("video/") { videoThumbnails.append(TelegramMediaFile.VideoThumbnail(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource)) } else { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: thumbnail.dimensions ?? PixelDimensions(width: 128, height: 128), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } var fileName = "file" diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift index ef1b48f563a..329902ac3fe 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ScheduledMessages.swift @@ -108,7 +108,7 @@ func managedApplyPendingScheduledMessagesActions(postbox: Postbox, network: Netw addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start() } } |> ignoreValues diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 76c0cf483c6..82be33acaf4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -409,8 +409,8 @@ public extension TelegramEngine { return _internal_sendWebViewData(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId, buttonText: buttonText, data: data) } - public func addBotToAttachMenu(botId: PeerId) -> Signal { - return _internal_addBotToAttachMenu(postbox: self.account.postbox, network: self.account.network, botId: botId) + public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal { + return _internal_addBotToAttachMenu(postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite) } public func removeBotFromAttachMenu(botId: PeerId) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 9213f937292..523ad9a3e94 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -86,6 +86,7 @@ public enum AdminLogEventAction { case editTopic(prevInfo: ForumTopicInfo, newInfo: ForumTopicInfo) case pinTopic(prevInfo: EngineMessageHistoryThread.Info?, newInfo: EngineMessageHistoryThread.Info?) case toggleForum(isForum: Bool) + case toggleAntiSpam(isEnabled: Bool) } public enum ChannelAdminLogEventError { @@ -342,6 +343,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m action = .pinTopic(prevInfo: prevInfo, newInfo: newInfo) case let .channelAdminLogEventActionToggleForum(newValue): action = .toggleForum(isForum: newValue == .boolTrue) + case let .channelAdminLogEventActionToggleAntiSpam(newValue): + action = .toggleAntiSpam(isEnabled: newValue == .boolTrue) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift index 4b2847d7786..77238d619d6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationSoundList.swift @@ -152,7 +152,7 @@ public func ensureDownloadedNotificationSoundList(postbox: Postbox) -> Signal ignoreValues |> `catch` { _ -> Signal in return .complete() @@ -228,7 +228,7 @@ private func pollNotificationSoundList(postbox: Postbox, network: Network) -> Si for resource in resources { signals.append( - fetchedMediaResource(mediaBox: postbox.mediaBox, reference: .soundList(resource: resource)) + fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .file, reference: .soundList(resource: resource)) |> ignoreValues |> `catch` { _ -> Signal in return .complete() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index 06f7d4bbdbc..a5d362c9e56 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -14,8 +14,18 @@ public enum UploadPeerPhotoError { case generic } -func _internal_updateAccountPhoto(account: Account, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +func _internal_updateAccountPhoto(account: Account, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, fallback: Bool, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: account.peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, fallback: fallback, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +} + +public enum SetCustomPeerPhotoMode { + case custom + case suggest + case customAndSuggest +} + +func _internal_updateContactPhoto(account: Account, peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: account.postbox, network: account.network, stateManager: account.stateManager, accountPeerId: account.peerId, peerId: peerId, photo: resource.flatMap({ _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: $0) }), video: videoResource.flatMap({ _internal_uploadedPeerVideo(postbox: account.postbox, network: account.network, messageMediaPreuploadManager: account.messageMediaPreuploadManager, resource: $0) |> map(Optional.init) }), videoStartTimestamp: videoStartTimestamp, customPeerPhotoMode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } public struct UploadedPeerPhotoData { @@ -66,11 +76,11 @@ func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMedi } } -func _internal_updatePeerPhoto(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updatePeerPhotoInternal(postbox: postbox, network: network, stateManager: stateManager, accountPeerId: accountPeerId, peer: postbox.loadedPeerWithId(peerId), photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: mapResourceToAvatarSizes) +func _internal_updatePeerPhoto(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, fallback: Bool = false, customPeerPhotoMode: SetCustomPeerPhotoMode? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhotoInternal(postbox: postbox, network: network, stateManager: stateManager, accountPeerId: accountPeerId, peer: postbox.loadedPeerWithId(peerId), photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, fallback: fallback, customPeerPhotoMode: customPeerPhotoMode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) } -func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peer: Signal, photo: Signal?, video: Signal?, videoStartTimestamp: Double?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { +func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, stateManager: AccountStateManager?, accountPeerId: PeerId, peer: Signal, photo: Signal?, video: Signal?, videoStartTimestamp: Double?, fallback: Bool = false, customPeerPhotoMode: SetCustomPeerPhotoMode? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { return peer |> mapError { _ -> UploadPeerPhotoError in } |> mapToSignal { peer -> Signal in @@ -131,6 +141,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state } } } + if peer is TelegramUser { var flags: Int32 = (1 << 0) if let _ = videoFile { @@ -139,14 +150,39 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state flags |= (1 << 2) } } + let request: Signal + if peer.id == accountPeerId { + if fallback { + flags |= (1 << 3) + } + request = network.request(Api.functions.photos.uploadProfilePhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)) + } else if let inputUser = apiInputUser(peer) { + if let customPeerPhotoMode = customPeerPhotoMode { + switch customPeerPhotoMode { + case .custom: + flags |= (1 << 4) + case .suggest: + flags |= (1 << 3) + case .customAndSuggest: + flags |= (1 << 3) + flags |= (1 << 4) + } + } + + request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: file, video: videoFile, videoStartTs: videoStartTimestamp)) + } else { + request = .complete() + } - return network.request(Api.functions.photos.uploadProfilePhoto(flags: flags, file: file, video: videoFile, videoStartTs: videoStartTimestamp)) + return request |> mapError { _ in return UploadPeerPhotoError.generic } |> mapToSignal { photo -> Signal<(UpdatePeerPhotoStatus, MediaResource?, MediaResource?), UploadPeerPhotoError> in var representations: [TelegramMediaImageRepresentation] = [] var videoRepresentations: [TelegramMediaImage.VideoRepresentation] = [] + var image: TelegramMediaImage? switch photo { - case let .photo(photo: apiPhoto, users: _): + case let .photo(apiPhoto, _): + image = telegramMediaImageFromApiPhoto(apiPhoto) switch apiPhoto { case .photoEmpty: break @@ -158,9 +194,9 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state for size in sizes { switch size { case let .photoSize(_, w, h, _): - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSizeProgressive(_, w, h, sizes): - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: w <= 200 ? .small : .fullSize, volumeId: nil, localId: nil), progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) default: break } @@ -193,11 +229,33 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state if let peer = transaction.getPeer(peer.id) { updatePeers(transaction: transaction, peers: [peer], update: { (_, peer) -> Peer? in if let peer = peer as? TelegramUser { - return peer.withUpdatedPhoto(representations) + if customPeerPhotoMode == .suggest || fallback { + return peer + } else { + return peer.withUpdatedPhoto(representations) + } } else { return peer } }) + + if fallback { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedFallbackPhoto(.known(image)) + } else { + return nil + } + } + } else if let customPeerPhotoMode = customPeerPhotoMode, case .custom = customPeerPhotoMode { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedPersonalPhoto(.known(image)) + } else { + return nil + } + } + } } return (.complete(representations), photoResult.resource, videoResult?.resource) } |> mapError { _ -> UploadPeerPhotoError in } @@ -289,13 +347,55 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state } } else { if let _ = peer as? TelegramUser { - let signal: Signal = network.request(Api.functions.photos.updateProfilePhoto(id: Api.InputPhoto.inputPhotoEmpty)) + let request: Signal + if peer.id == accountPeerId { + var flags: Int32 = 0 + if fallback { + flags |= (1 << 0) + } + request = network.request(Api.functions.photos.updateProfilePhoto(flags: flags, id: Api.InputPhoto.inputPhotoEmpty)) + } else if let inputUser = apiInputUser(peer) { + let flags: Int32 = (1 << 4) + request = network.request(Api.functions.photos.uploadContactProfilePhoto(flags: flags, userId: inputUser, file: nil, video: nil, videoStartTs: nil)) + } else { + request = .complete() + } + + return request |> mapError { _ -> UploadPeerPhotoError in return .generic } - - return signal - |> mapToSignal { _ -> Signal in + |> mapToSignal { photo -> Signal in + if peer.id != accountPeerId { + var updatedUsers: [TelegramUser] = [] + switch photo { + case let .photo(_, apiUsers): + updatedUsers = apiUsers.map { TelegramUser(user: $0) } + } + return postbox.transaction { transaction -> UpdatePeerPhotoStatus in + updatePeers(transaction: transaction, peers: updatedUsers, update: { (_, updatedPeer) -> Peer? in + return updatedPeer + }) + if fallback { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedFallbackPhoto(.known(nil)) + } else { + return nil + } + } + } else if let customPeerPhotoMode = customPeerPhotoMode, case .custom = customPeerPhotoMode { + transaction.updatePeerCachedData(peerIds: Set([peer.id])) { peerId, cachedPeerData in + if let cachedPeerData = cachedPeerData as? CachedUserData { + return cachedPeerData.withUpdatedPersonalPhoto(.known(nil)) + } else { + return nil + } + } + } + return .complete([]) + } |> mapError { _ -> UploadPeerPhotoError in } + } return .single(.complete([])) } } else { @@ -348,7 +448,7 @@ func _internal_updatePeerPhotoInternal(postbox: Postbox, network: Network, state func _internal_updatePeerPhotoExisting(network: Network, reference: TelegramMediaImageReference) -> Signal { switch reference { case let .cloud(imageId, accessHash, fileReference): - return network.request(Api.functions.photos.updateProfilePhoto(id: .inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference)))) + return network.request(Api.functions.photos.updateProfilePhoto(flags: 0, id: .inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference)))) |> `catch` { _ -> Signal in return .complete() } @@ -362,12 +462,12 @@ func _internal_updatePeerPhotoExisting(network: Network, reference: TelegramMedi } } -func _internal_removeAccountPhoto(network: Network, reference: TelegramMediaImageReference?) -> Signal { +func _internal_removeAccountPhoto(account: Account, reference: TelegramMediaImageReference?, fallback: Bool) -> Signal { if let reference = reference { switch reference { case let .cloud(imageId, accessHash, fileReference): if let fileReference = fileReference { - return network.request(Api.functions.photos.deletePhotos(id: [.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference))])) + return account.network.request(Api.functions.photos.deletePhotos(id: [.inputPhoto(id: imageId, accessHash: accessHash, fileReference: Buffer(data: fileReference))])) |> `catch` { _ -> Signal<[Int64], NoError> in return .single([]) } @@ -379,7 +479,29 @@ func _internal_removeAccountPhoto(network: Network, reference: TelegramMediaImag } } } else { - let api = Api.functions.photos.updateProfilePhoto(id: Api.InputPhoto.inputPhotoEmpty) - return network.request(api) |> map { _ in } |> retryRequest + var flags: Int32 = 0 + if fallback { + flags |= (1 << 0) + } + let api = Api.functions.photos.updateProfilePhoto(flags: flags, id: Api.InputPhoto.inputPhotoEmpty) + return account.network.request(api) + |> map { _ in } + |> retryRequest + |> mapToSignal { _ -> Signal in + if fallback { + return account.postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + if let current = current as? CachedUserData { + return current.withUpdatedFallbackPhoto(.known(nil)) + } else { + return current + } + }) + return Void() + } + } else { + return .complete() + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 7952dd019fe..349756bfbfc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1,6 +1,7 @@ import Foundation import SwiftSignalKit import Postbox +import TelegramApi public enum AddressNameValidationStatus: Equatable { case checking @@ -981,6 +982,37 @@ public extension TelegramEngine { public func exportContactToken() -> Signal { return _internal_exportContactToken(account: self.account) } + + public func updateChannelMembersHidden(peerId: EnginePeer.Id, value: Bool) -> Signal { + return self.account.postbox.transaction { transaction -> Api.InputChannel? in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + if let current = current as? CachedChannelData { + return current.withUpdatedMembersHidden(.known(PeerMembersHidden(value: value))) + } else { + return current + } + }) + + return transaction.getPeer(peerId).flatMap(apiInputChannel) + } + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .complete() + } + + return self.account.network.request(Api.functions.channels.toggleParticipantsHidden(channel: inputChannel, enabled: value ? .boolTrue : .boolFalse)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> beforeNext { updates in + if let updates = updates { + self.account.stateManager.addUpdates(updates) + } + } + |> ignoreValues + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 473c8f46201..b7e640e23d5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -212,7 +212,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } switch fullUser { - case let .userFull(_, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _): + case let .userFull(_, _, _, _, _, _, _, userFullNotifySettings, _, _, _, _, _, _, _, _, _, _): updatePeers(transaction: transaction, peers: peers, update: { previous, updated -> Peer in if previous?.id == accountPeerId, let accountUser = accountUser, let user = TelegramUser.merge(previous as? TelegramUser, rhs: accountUser) { return user @@ -230,7 +230,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee previous = CachedUserData() } switch fullUser { - case let .userFull(userFullFlags, _, userFullAbout, userFullSettings, profilePhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions): + case let .userFull(userFullFlags, _, userFullAbout, userFullSettings, personalPhoto, profilePhoto, fallbackPhoto, _, userFullBotInfo, userFullPinnedMsgId, userFullCommonChatsCount, _, userFullTtlPeriod, userFullThemeEmoticon, _, _, _, userPremiumGiftOptions): let botInfo = userFullBotInfo.flatMap(BotInfo.init(apiBotInfo:)) let isBlocked = (userFullFlags & (1 << 0)) != 0 let voiceCallsAvailable = (userFullFlags & (1 << 4)) != 0 @@ -248,7 +248,9 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let autoremoveTimeout: CachedPeerAutoremoveTimeout = .known(CachedPeerAutoremoveTimeout.Value(userFullTtlPeriod)) + let personalPhoto = personalPhoto.flatMap { telegramMediaImageFromApiPhoto($0) } let photo = profilePhoto.flatMap { telegramMediaImageFromApiPhoto($0) } + let fallbackPhoto = fallbackPhoto.flatMap { telegramMediaImageFromApiPhoto($0) } let premiumGiftOptions: [CachedPremiumGiftOption] if let userPremiumGiftOptions = userPremiumGiftOptions { @@ -268,6 +270,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedAutoremoveTimeout(autoremoveTimeout) .withUpdatedThemeEmoticon(userFullThemeEmoticon) .withUpdatedPhoto(.known(photo)) + .withUpdatedPersonalPhoto(.known(personalPhoto)) + .withUpdatedFallbackPhoto(.known(fallbackPhoto)) .withUpdatedPremiumGiftOptions(premiumGiftOptions) .withUpdatedVoiceMessagesAvailable(voiceMessagesAvailable) } @@ -627,6 +631,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee mappedAllowedReactions = .empty } + let membersHidden = (flags2 & (1 << 2)) != 0 + return previous.withUpdatedFlags(channelFlags) .withUpdatedAbout(about) .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) @@ -653,6 +659,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) .withUpdatedAllowedReactions(.known(mappedAllowedReactions)) + .withUpdatedMembersHidden(.known(PeerMembersHidden(value: membersHidden))) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { @@ -661,7 +668,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds) }) if !resourceIds.isEmpty { - let _ = postbox.mediaBox.removeCachedResources(Set(resourceIds)).start() + let _ = postbox.mediaBox.removeCachedResources(Array(Set(resourceIds))).start() } } case .chatFull: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index cc1e9fe5fc9..a871b81bcc4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -1,7 +1,8 @@ import Foundation import Postbox import SwiftSignalKit - +import MtProtoKit +import DarwinDirStat public enum PeerCacheUsageCategory: Int32 { case image = 0 @@ -52,8 +53,742 @@ private final class CacheUsageStatsState { var upperBound: MessageIndex? } +public final class StorageUsageStats { + public enum CategoryKey: Hashable { + case photos + case videos + case files + case music + case stickers + case avatars + case misc + } + + public struct CategoryData { + public var size: Int64 + public var messages: [EngineMessage.Id: Int64] + + public init(size: Int64, messages: [EngineMessage.Id: Int64]) { + self.size = size + self.messages = messages + } + } + + public fileprivate(set) var categories: [CategoryKey: CategoryData] + + public init(categories: [CategoryKey: CategoryData]) { + self.categories = categories + } +} + +public final class AllStorageUsageStats { + public final class PeerStats { + public let peer: EnginePeer + public let stats: StorageUsageStats + + public init(peer: EnginePeer, stats: StorageUsageStats) { + self.peer = peer + self.stats = stats + } + } + + public var deviceAvailableSpace: Int64 + public var deviceFreeSpace: Int64 + public fileprivate(set) var totalStats: StorageUsageStats + public fileprivate(set) var peers: [EnginePeer.Id: PeerStats] + + public init(deviceAvailableSpace: Int64, deviceFreeSpace: Int64, totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) { + self.deviceAvailableSpace = deviceAvailableSpace + self.deviceFreeSpace = deviceFreeSpace + self.totalStats = totalStats + self.peers = peers + } +} + +private extension StorageUsageStats { + convenience init(_ stats: StorageBox.Stats) { + var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:] + for (key, value) in stats.contentTypes { + let mappedCategory: StorageUsageStats.CategoryKey + switch key { + case MediaResourceUserContentType.image.rawValue: + mappedCategory = .photos + case MediaResourceUserContentType.video.rawValue: + mappedCategory = .videos + case MediaResourceUserContentType.file.rawValue: + mappedCategory = .files + case MediaResourceUserContentType.audio.rawValue: + mappedCategory = .music + case MediaResourceUserContentType.avatar.rawValue: + mappedCategory = .avatars + case MediaResourceUserContentType.sticker.rawValue: + mappedCategory = .stickers + default: + mappedCategory = .misc + } + mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value.size, messages: value.messages) + } + + self.init(categories: mappedCategories) + } +} + +func _internal_collectStorageUsageStats(account: Account) -> Signal { + let additionalStats = Signal { subscriber in + DispatchQueue.global().async { + var totalSize: Int64 = 0 + + let additionalPaths: [String] = [ + "cache", + "animation-cache", + "short-cache", + ] + + func statForDirectory(path: String) -> Int64 { + var s = darwin_dirstat() + var result = dirstat_np(path, 1, &s, MemoryLayout.size) + if result != -1 { + return Int64(s.total_size) + } else { + result = dirstat_np(path, 0, &s, MemoryLayout.size) + if result != -1 { + return Int64(s.total_size) + } else { + return 0 + } + } + } + + var delayedDirs: [String] = [] + + for path in additionalPaths { + let fullPath: String + if path.isEmpty { + fullPath = account.postbox.mediaBox.basePath + } else { + fullPath = account.postbox.mediaBox.basePath + "/\(path)" + } + + if path == "animation-cache" { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: fullPath), includingPropertiesForKeys: [.isDirectoryKey], options: .skipsSubdirectoryDescendants) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + delayedDirs.append(fullPath + "/" + url.lastPathComponent) + } + } + } else { + totalSize += statForDirectory(path: fullPath) + } + } + + if !delayedDirs.isEmpty { + let concurrentSize = Atomic<[Int64]>(value: []) + + DispatchQueue.concurrentPerform(iterations: delayedDirs.count, execute: { index in + let directorySize = statForDirectory(path: delayedDirs[index]) + let result = concurrentSize.modify { current in + return current + [directorySize] + } + if result.count == delayedDirs.count { + var aggregatedCount: Int64 = 0 + for item in result { + aggregatedCount += item + } + subscriber.putNext(totalSize + aggregatedCount) + subscriber.putCompletion() + } + }) + } else { + subscriber.putNext(totalSize) + subscriber.putCompletion() + } + } + + return EmptyDisposable + } + + return combineLatest( + additionalStats, + account.postbox.mediaBox.storageBox.getAllStats() + ) + |> deliverOnMainQueue + |> mapToSignal { additionalStats, allStats -> Signal in + return account.postbox.transaction { transaction -> AllStorageUsageStats in + let total = StorageUsageStats(allStats.total) + if additionalStats != 0 { + if total.categories[.misc] == nil { + total.categories[.misc] = StorageUsageStats.CategoryData(size: 0, messages: [:]) + } + total.categories[.misc]?.size += additionalStats + } + + var peers: [EnginePeer.Id: AllStorageUsageStats.PeerStats] = [:] + + for (peerId, peerStats) in allStats.peers { + if peerId.id._internalGetInt64Value() == 0 { + continue + } + + var peerSize: Int64 = 0 + for (_, contentValue) in peerStats.contentTypes { + peerSize += contentValue.size + } + if peerSize == 0 { + continue + } + + if let peer = transaction.getPeer(peerId), transaction.getPeerChatListIndex(peerId) != nil { + peers[peerId] = AllStorageUsageStats.PeerStats( + peer: EnginePeer(peer), + stats: StorageUsageStats(peerStats) + ) + } + } + + let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) + let deviceAvailableSpace = (systemAttributes?[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0 + let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 + + return AllStorageUsageStats( + deviceAvailableSpace: deviceAvailableSpace, + deviceFreeSpace: deviceFreeSpace, + totalStats: total, + peers: peers + ) + } + } +} + +func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> { + return account.postbox.transaction { transaction -> [EngineMessage.Id: Message] in + var result: [EngineMessage.Id: Message] = [:] + for (category, value) in stats.categories { + if !categories.contains(category) { + continue + } + + for (id, _) in value.messages.sorted(by: { $0.value >= $1.value }).prefix(1000) { + if result[id] == nil { + if let message = existingMessages[id] { + result[id] = message + } else if let message = transaction.getMessage(id) { + result[id] = message + } + } + } + } + + return result + } +} + +func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal { + let mediaBox = account.postbox.mediaBox + return Signal { subscriber in + mediaBox.storageBox.remove(peerId: peerId, contentTypes: categories.map { item -> UInt8 in + let mappedItem: MediaResourceUserContentType + switch item { + case .photos: + mappedItem = .image + case .videos: + mappedItem = .video + case .files: + mappedItem = .file + case .music: + mappedItem = .audio + case .stickers: + mappedItem = .sticker + case .avatars: + mappedItem = .avatar + case .misc: + mappedItem = .other + } + return mappedItem.rawValue + }, completion: { ids in + var resourceIds: [MediaResourceId] = [] + for id in ids { + if let value = String(data: id, encoding: .utf8) { + resourceIds.append(MediaResourceId(value)) + } + } + let _ = mediaBox.removeCachedResources(resourceIds).start(completed: { + if peerId == nil && categories.contains(.misc) { + let additionalPaths: [String] = [ + "cache", + "animation-cache", + "short-cache", + ] + + for item in additionalPaths { + let fullPath = mediaBox.basePath + "/\(item)" + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: fullPath), includingPropertiesForKeys: [.isDirectoryKey], options: .skipsSubdirectoryDescendants) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + let _ = try? FileManager.default.removeItem(at: url) + } + } + } + + subscriber.putCompletion() + } else { + subscriber.putCompletion() + } + }) + }) + + return ActionDisposable { + } + } +} + +func _internal_clearStorage(account: Account, peerIds: Set) -> Signal { + let mediaBox = account.postbox.mediaBox + return Signal { subscriber in + mediaBox.storageBox.remove(peerIds: peerIds, completion: { ids in + var resourceIds: [MediaResourceId] = [] + for id in ids { + if let value = String(data: id, encoding: .utf8) { + resourceIds.append(MediaResourceId(value)) + } + } + let _ = mediaBox.removeCachedResources(resourceIds).start(completed: { + subscriber.putCompletion() + }) + }) + + return ActionDisposable { + } + } +} + +func _internal_clearStorage(account: Account, messages: [Message]) -> Signal { + let mediaBox = account.postbox.mediaBox + + return Signal { subscriber in + DispatchQueue.global().async { + var resourceIds = Set() + for message in messages { + for media in message.media { + if let image = media as? TelegramMediaImage { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } else if let file = media as? TelegramMediaFile { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } else if let webpage = media as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if let image = content.image { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } + if let file = content.file { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } + } + } else if let game = media as? TelegramMediaGame { + if let image = game.image { + for representation in image.representations { + resourceIds.insert(representation.resource.id) + } + } + if let file = game.file { + for representation in file.previewRepresentations { + resourceIds.insert(representation.resource.id) + } + resourceIds.insert(file.resource.id) + } + } + } + } + + var removeIds: [Data] = [] + for resourceId in resourceIds { + if let id = resourceId.stringRepresentation.data(using: .utf8) { + removeIds.append(id) + } + } + + mediaBox.storageBox.remove(ids: removeIds) + let _ = mediaBox.removeCachedResources(Array(resourceIds)).start(completed: { + subscriber.putCompletion() + }) + } + + return ActionDisposable { + } + } +} + +func _internal_reindexCacheInBackground(account: Account, lowImpact: Bool) -> Signal { + let postbox = account.postbox + + let queue = Queue(name: "ReindexCacheInBackground") + return Signal { subscriber in + let isCancelled = Atomic(value: false) + + func process(lowerBound: MessageIndex?) { + if isCancelled.with({ $0 }) { + return + } + + let _ = (postbox.transaction { transaction -> (messagesByMediaId: [MediaId: [MessageId]], mediaMap: [MediaId: Media], nextLowerBound: MessageIndex?) in + return transaction.enumerateMediaMessages(lowerBound: lowerBound, upperBound: nil, limit: 1000) + } + |> deliverOn(queue)).start(next: { result in + Logger.shared.log("ReindexCacheInBackground", "process batch of \(result.mediaMap.count) media") + + var storageItems: [(reference: StorageBox.Reference, id: Data, contentType: UInt8, size: Int64)] = [] + + let mediaBox = postbox.mediaBox + + let processResource: ([MessageId], MediaResource, MediaResourceUserContentType) -> Void = { messageIds, resource, contentType in + let size = mediaBox.fileSizeForId(resource.id) + if size != 0 { + if let itemId = resource.id.stringRepresentation.data(using: .utf8) { + for messageId in messageIds { + storageItems.append((reference: StorageBox.Reference(peerId: messageId.peerId.toInt64(), messageNamespace: UInt8(clamping: messageId.namespace), messageId: messageId.id), id: itemId, contentType: contentType.rawValue, size: size)) + } + } + } + } + + for (_, media) in result.mediaMap { + guard let mediaId = media.id else { + continue + } + guard let mediaMessages = result.messagesByMediaId[mediaId] else { + continue + } + + if let image = media as? TelegramMediaImage { + for representation in image.representations { + processResource(mediaMessages, representation.resource, .image) + } + } else if let file = media as? TelegramMediaFile { + for representation in file.previewRepresentations { + processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) + } + processResource(mediaMessages, file.resource, MediaResourceUserContentType(file: file)) + } else if let webpage = media as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if let image = content.image { + for representation in image.representations { + processResource(mediaMessages, representation.resource, .image) + } + } + if let file = content.file { + for representation in file.previewRepresentations { + processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) + } + processResource(mediaMessages, file.resource, MediaResourceUserContentType(file: file)) + } + } + } else if let game = media as? TelegramMediaGame { + if let image = game.image { + for representation in image.representations { + processResource(mediaMessages, representation.resource, .image) + } + } + if let file = game.file { + for representation in file.previewRepresentations { + processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) + } + processResource(mediaMessages, file.resource, MediaResourceUserContentType(file: file)) + } + } + } + + if !storageItems.isEmpty { + mediaBox.storageBox.batchAdd(items: storageItems) + } + + if let nextLowerBound = result.nextLowerBound { + if lowImpact { + queue.after(0.4, { + process(lowerBound: nextLowerBound) + }) + } else { + process(lowerBound: nextLowerBound) + } + } else { + subscriber.putCompletion() + } + }) + } + + process(lowerBound: nil) + + return ActionDisposable { + let _ = isCancelled.swap(true) + } + } + |> runOn(queue) +} + func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { - let initialState = CacheUsageStatsState() + return account.postbox.mediaBox.storageBox.all() + |> mapToSignal { entries -> Signal in + final class IncrementalState { + var startIndex: Int = 0 + + var media: [PeerId: [PeerCacheUsageCategory: [MediaId: Int64]]] = [:] + var mediaResourceIds: [MediaId: [MediaResourceId]] = [:] + var totalSize: Int64 = 0 + var mediaSize: Int64 = 0 + + var processedResourceIds = Set() + + var otherSize: Int64 = 0 + var otherPaths: [String] = [] + + var peers: [PeerId: Peer] = [:] + } + + let mediaBox = account.postbox.mediaBox + + let queue = Queue() + return Signal { subscriber in + var isCancelled: Bool = false + + let state = Atomic(value: IncrementalState()) + + var processNextBatchPtr: (() -> Void)? + let processNextBatch: () -> Void = { + if isCancelled { + return + } + + let _ = (account.postbox.transaction { transaction -> Void in + state.with { state in + if state.startIndex >= entries.count { + return + } + + let batchCount = 5000 + let endIndex = min(state.startIndex + batchCount, entries.count) + for i in state.startIndex ..< endIndex { + let entry = entries[i] + + guard let resourceIdString = String(data: entry.id, encoding: .utf8) else { + continue + } + let resourceId = MediaResourceId(resourceIdString) + if state.processedResourceIds.contains(resourceId.stringRepresentation) { + continue + } + + let resourceSize = mediaBox.resourceUsage(id: resourceId) + if resourceSize != 0 { + state.totalSize += resourceSize + + for reference in entry.references { + if reference.peerId == 0 { + state.otherSize += resourceSize + + let storePaths = mediaBox.storePathsForId(resourceId) + state.otherPaths.append(storePaths.complete) + state.otherPaths.append(storePaths.partial) + + continue + } + if let message = transaction.getMessage(MessageId(peerId: PeerId(reference.peerId), namespace: MessageId.Namespace(reference.messageNamespace), id: reference.messageId)) { + for mediaItem in message.media { + guard let mediaId = mediaItem.id else { + continue + } + var category: PeerCacheUsageCategory? + if let _ = mediaItem as? TelegramMediaImage { + category = .image + } else if let mediaItem = mediaItem as? TelegramMediaFile { + if mediaItem.isMusic || mediaItem.isVoice { + category = .audio + } else if mediaItem.isVideo { + category = .video + } else { + category = .file + } + } + if let category = category { + state.mediaSize += resourceSize + state.processedResourceIds.insert(resourceId.stringRepresentation) + + state.media[PeerId(reference.peerId), default: [:]][category, default: [:]][mediaId, default: 0] += resourceSize + if let index = state.mediaResourceIds.index(forKey: mediaId) { + if !state.mediaResourceIds[index].value.contains(resourceId) { + state.mediaResourceIds[mediaId]?.append(resourceId) + } + } else { + state.mediaResourceIds[mediaId] = [resourceId] + } + } + } + } + } + } + } + state.startIndex = endIndex + } + }).start(completed: { + if isCancelled { + return + } + let isFinished = state.with { state -> Bool in + return state.startIndex >= entries.count + } + if !isFinished { + queue.async { + processNextBatchPtr?() + } + } else { + let _ = (account.postbox.transaction { transaction -> Void in + state.with { state in + for peerId in state.media.keys { + if let peer = transaction.getPeer(peerId) { + state.peers[peer.id] = peer + } + } + } + }).start(completed: { + queue.async { + let state = state.with { $0 } + var tempPaths: [String] = [] + var tempSize: Int64 = 0 + #if os(iOS) + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: NSTemporaryDirectory()), includingPropertiesForKeys: [.isDirectoryKey, .fileAllocatedSizeKey, .isSymbolicLinkKey]) { + for url in enumerator { + if let url = url as? URL { + if let isDirectoryValue = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectoryValue { + tempPaths.append(url.path) + } else if let fileSizeValue = (try? url.resourceValues(forKeys: Set([.fileAllocatedSizeKey])))?.fileAllocatedSize { + tempPaths.append(url.path) + + if let isSymbolicLinkValue = (try? url.resourceValues(forKeys: Set([.isSymbolicLinkKey])))?.isSymbolicLink, isSymbolicLinkValue { + } else { + tempSize += Int64(fileSizeValue) + } + } + } + } + } + #endif + + var immutableSize: Int64 = 0 + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: account.basePath + "/postbox/db"), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { + for url in files { + if let fileSize = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize { + immutableSize += Int64(fileSize) + } + } + } + if let logFilesPath = logFilesPath, let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logFilesPath), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { + for url in files { + if let fileSize = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize { + immutableSize += Int64(fileSize) + } + } + } + + for additionalPath in additionalCachePaths { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: additionalPath), includingPropertiesForKeys: [.isDirectoryKey, .fileAllocatedSizeKey, .isSymbolicLinkKey]) { + for url in enumerator { + if let url = url as? URL { + if let isDirectoryValue = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectoryValue { + } else if let fileSizeValue = (try? url.resourceValues(forKeys: Set([.fileAllocatedSizeKey])))?.fileAllocatedSize { + tempPaths.append(url.path) + + if let isSymbolicLinkValue = (try? url.resourceValues(forKeys: Set([.isSymbolicLinkKey])))?.isSymbolicLink, isSymbolicLinkValue { + } else { + tempSize += Int64(fileSizeValue) + } + } + } + } + } + } + + var cacheSize: Int64 = 0 + let basePath = account.postbox.mediaBox.basePath + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + state.otherPaths.append("cache/" + url.lastPathComponent) + cacheSize += Int64(value) + } + } + } + } + + func processRecursive(directoryPath: String, subdirectoryPath: String) { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory { + processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)") + } else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + state.otherPaths.append("\(subdirectoryPath)/" + url.lastPathComponent) + cacheSize += Int64(value) + } + } + } + } + } + + processRecursive(directoryPath: basePath + "/animation-cache", subdirectoryPath: "animation-cache") + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + state.otherPaths.append("short-cache/" + url.lastPathComponent) + cacheSize += Int64(value) + } + } + } + } + + subscriber.putNext(.result(CacheUsageStats( + media: state.media, + mediaResourceIds: state.mediaResourceIds, + peers: state.peers, + otherSize: state.otherSize, + otherPaths: state.otherPaths, + cacheSize: cacheSize, + tempPaths: tempPaths, + tempSize: tempSize, + immutableSize: immutableSize + ))) + subscriber.putCompletion() + } + }) + } + }) + } + processNextBatchPtr = { + processNextBatch() + } + + processNextBatch() + + return ActionDisposable { + isCancelled = true + } + } + |> runOn(queue) + } + + /*let initialState = CacheUsageStatsState() if let peerId = peerId { initialState.lowerBound = MessageIndex.lowerBound(peerId: peerId) initialState.upperBound = MessageIndex.upperBound(peerId: peerId) @@ -277,7 +1012,8 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a let signal = (fetch |> mapToSignal { mediaByPeer, mediaRefs, updatedLowerBound -> Signal in return process(mediaByPeer, mediaRefs, updatedLowerBound) - }) |> restart + }) + |> restart return signal |> `catch` { error in switch error { @@ -287,9 +1023,9 @@ func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, a return .complete() } } - } + }*/ } func _internal_clearCachedMediaResources(account: Account, mediaResourceIds: Set) -> Signal { - return account.postbox.mediaBox.removeCachedResources(mediaResourceIds) + return account.postbox.mediaBox.removeCachedResources(Array(mediaResourceIds)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index 82e7fd11e16..4f04bfab395 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -6,6 +6,26 @@ import TelegramApi public typealias EngineTempBox = TempBox public typealias EngineTempBoxFile = TempBoxFile +public extension MediaResourceUserContentType { + init(file: TelegramMediaFile) { + if file.isMusic || file.isVoice { + self = .audio + } else if file.isSticker || file.isAnimatedSticker { + self = .sticker + } else if file.isCustomEmoji { + self = .sticker + } else if file.isVideo { + if file.isAnimated { + self = .other + } else { + self = .video + } + } else { + self = .file + } + } +} + func bufferedFetch(_ signal: Signal) -> Signal { return Signal { subscriber in final class State { @@ -202,10 +222,41 @@ public extension TelegramEngine { public func collectCacheUsageStats(peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { return _internal_collectCacheUsageStats(account: self.account, peerId: peerId, additionalCachePaths: additionalCachePaths, logFilesPath: logFilesPath) } + + public func collectStorageUsageStats() -> Signal { + return _internal_collectStorageUsageStats(account: self.account) + } + + public func renderStorageUsageStatsMessages(stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> { + return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: existingMessages) + } + + public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal { + return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories) + } + + public func clearStorage(peerIds: Set) -> Signal { + _internal_clearStorage(account: self.account, peerIds: peerIds) + } + + public func clearStorage(messages: [Message]) -> Signal { + _internal_clearStorage(account: self.account, messages: messages) + } public func clearCachedMediaResources(mediaResourceIds: Set) -> Signal { return _internal_clearCachedMediaResources(account: self.account, mediaResourceIds: mediaResourceIds) } + + public func reindexCacheInBackground(lowImpact: Bool) -> Signal { + let mediaBox = self.account.postbox.mediaBox + + return _internal_reindexCacheInBackground(account: self.account, lowImpact: lowImpact) + |> then(Signal { subscriber in + return mediaBox.updateResourceIndex(lowImpact: lowImpact, completion: { + subscriber.putCompletion() + }) + }) + } public func data(id: EngineMediaResource.Id, attemptSynchronously: Bool = false) -> Signal { return self.account.postbox.mediaBox.resourceData( @@ -293,6 +344,8 @@ public extension TelegramEngine { preferBackgroundReferenceRevalidation: false, continueInBackground: false ), + location: nil, + contentType: .image, isRandomAccessAllowed: true )) |> map { result -> EngineMediaResource.Fetch.Result in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index 9a13ff10f76..a630f1d10de 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -11,13 +11,13 @@ func telegramStickerPackThumbnailRepresentationFromApiSizes(datacenterId: Int32, switch size { case let .photoCachedSize(_, w, h, _): let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSize(_, w, h, _): let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoSizeProgressive(_, w, h, sizes): let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, thumbVersion: thumbVersion, volumeId: nil, localId: nil) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case let .photoPathSize(_, data): immediateThumbnailData = data.makeData() case .photoStrippedSize: @@ -103,7 +103,7 @@ func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaRefe |> map { result -> [StickerPackReference] in return result.map { pack in switch pack { - case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _, _): + case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _, _), let .stickerSetNoCovered(set): let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks) return .id(id: info.id.id, accessHash: info.accessHash) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index a6349bf5001..9653113edf9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -168,12 +168,12 @@ func _internal_installStickerSetInteractively(account: Account, info: StickerPac return .generic } |> mapToSignal { result -> Signal in - let addResult:InstallStickerSetResult + let addResult: InstallStickerSetResult switch result { case .stickerSetInstallResultSuccess: addResult = .successful case let .stickerSetInstallResultArchive(sets: archived): - var coveredSets:[CoveredStickerSet] = [] + var coveredSets: [CoveredStickerSet] = [] for archived in archived { let apiDocuments:[Api.Document] let apiSet:Api.StickerSet @@ -187,6 +187,9 @@ func _internal_installStickerSetInteractively(account: Account, info: StickerPac case let .stickerSetFullCovered(set, _, _, documents): apiSet = set apiDocuments = documents + case let .stickerSetNoCovered(set): + apiSet = set + apiDocuments = [] } let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks) diff --git a/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift new file mode 100644 index 00000000000..e5f5febbf8a --- /dev/null +++ b/submodules/TelegramCore/Sources/Utils/AutomaticCacheEviction.swift @@ -0,0 +1,202 @@ +import Foundation +import SwiftSignalKit +import Postbox + +final class AutomaticCacheEvictionContext { + private final class Impl { + private struct CombinedSettings: Equatable { + var categoryStorageTimeout: [CacheStorageSettings.PeerStorageCategory: Int32] + var exceptions: [AccountSpecificCacheStorageSettings.Value] + } + + let queue: Queue + let processingQueue: Queue + let accountManager: AccountManager + let postbox: Postbox + + var settingsDisposable: Disposable? + var processDisposable: Disposable? + + init(queue: Queue, accountManager: AccountManager, postbox: Postbox) { + self.queue = queue + self.processingQueue = Queue(name: "AutomaticCacheEviction-Processing", qos: .background) + self.accountManager = accountManager + self.postbox = postbox + + self.start() + } + + deinit { + self.settingsDisposable?.dispose() + self.processDisposable?.dispose() + } + + func start() { + self.settingsDisposable?.dispose() + self.processDisposable?.dispose() + + let cacheSettings = self.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> map { sharedData -> CacheStorageSettings in + let cacheSettings: CacheStorageSettings + if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = CacheStorageSettings.defaultSettings + } + + return cacheSettings + } + + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + let accountSpecificSettings = self.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + } + + self.settingsDisposable = (combineLatest(queue: self.queue, + cacheSettings, + accountSpecificSettings + ) + |> map { cacheSettings, accountSpecificSettings -> CombinedSettings in + return CombinedSettings( + categoryStorageTimeout: cacheSettings.categoryStorageTimeout, + exceptions: accountSpecificSettings.peerStorageTimeoutExceptions + ) + } + |> distinctUntilChanged + |> deliverOn(self.queue)).start(next: { [weak self] combinedSettings in + self?.restart(settings: combinedSettings) + }) + } + + private func restart(settings: CombinedSettings) { + self.processDisposable?.dispose() + + let processingQueue = self.processingQueue + let postbox = self.postbox + let mediaBox = self.postbox.mediaBox + + let _ = processingQueue + let _ = mediaBox + + self.processDisposable = (self.postbox.mediaBox.storageBox.allPeerIds() + |> mapToSignal { peerIds -> Signal in + return postbox.transaction { transaction -> [PeerId: CacheStorageSettings.PeerStorageCategory] in + var channelCategoryMapping: [PeerId: CacheStorageSettings.PeerStorageCategory] = [:] + for peerId in peerIds { + if peerId.namespace == Namespaces.Peer.CloudChannel { + var category: CacheStorageSettings.PeerStorageCategory = .channels + if let peer = transaction.getPeer(peerId) as? TelegramChannel, case .group = peer.info { + category = .groups + } + channelCategoryMapping[peerId] = category + } + } + + return channelCategoryMapping + } + |> mapToSignal { channelCategoryMapping -> Signal in + var signals: Signal = .complete() + + var matchingPeers = 0 + + for peerId in peerIds { + let timeout: Int32 + if let value = settings.exceptions.first(where: { $0.key == peerId }) { + timeout = value.value + } else { + switch peerId.namespace { + case Namespaces.Peer.CloudUser, Namespaces.Peer.SecretChat: + timeout = settings.categoryStorageTimeout[.privateChats] ?? Int32.max + case Namespaces.Peer.CloudGroup: + timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max + default: + if let category = channelCategoryMapping[peerId], case .groups = category { + timeout = settings.categoryStorageTimeout[.groups] ?? Int32.max + } else { + timeout = settings.categoryStorageTimeout[.channels] ?? Int32.max + } + } + } + + if timeout == Int32.max { + continue + } + + matchingPeers += 1 + + let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - timeout + //let minPeerTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + signals = signals |> then(mediaBox.storageBox.all(peerId: peerId) + |> mapToSignal { peerResourceIds -> Signal in + return Signal { subscriber in + var isCancelled = false + + processingQueue.justDispatch { + var removeIds: [MediaResourceId] = [] + var removeRawIds: [Data] = [] + var localCounter = 0 + for resourceId in peerResourceIds { + localCounter += 1 + if localCounter % 100 == 0 { + if isCancelled { + subscriber.putCompletion() + return + } + } + + removeRawIds.append(resourceId) + let id = MediaResourceId(String(data: resourceId, encoding: .utf8)!) + let resourceTimestamp = mediaBox.resourceUsageWithInfo(id: id) + if resourceTimestamp != 0 && resourceTimestamp < minPeerTimestamp { + removeIds.append(id) + } + } + + if !removeIds.isEmpty { + Logger.shared.log("AutomaticCacheEviction", "peer \(peerId): cleaning \(removeIds.count) resources") + + let _ = mediaBox.removeCachedResources(removeIds).start(completed: { + mediaBox.storageBox.remove(ids: removeRawIds) + + subscriber.putCompletion() + }) + } else { + subscriber.putCompletion() + } + } + + return ActionDisposable { + isCancelled = true + } + } + }) + } + + Logger.shared.log("AutomaticCacheEviction", "have \(matchingPeers) peers with data") + + return signals + } + }).start() + } + } + + private let queue: Queue + private let impl: QueueLocalObject + + init(postbox: Postbox, accountManager: AccountManager) { + let queue = Queue(name: "AutomaticCacheEviction") + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, accountManager: accountManager, postbox: postbox) + }) + } +} diff --git a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift index f8fc943be98..a410693ee45 100644 --- a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift +++ b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift @@ -189,7 +189,7 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me } } else { return Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: accountMediaBox, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() + let fetch = fetchedMediaResource(mediaBox: accountMediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() var didOutputBlurred = false let data = accountMediaBox.cachedResourceRepresentation(file.file.resource, representation: representation, complete: true, fetch: true, attemptSynchronously: true).start(next: { data in if data.complete { @@ -227,7 +227,7 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me } } else { return Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: accountMediaBox, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() + let fetch = fetchedMediaResource(mediaBox: accountMediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.wallpaper(wallpaper: WallpaperReference.slug(file.slug), resource: file.file.resource)).start() var didOutputBlurred = false let data = accountMediaBox.resourceData(file.file.resource).start(next: { data in if data.complete { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 2d027d39cc6..4940685ff5c 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -1280,7 +1280,8 @@ public func defaultBuiltinWallpaper(data: BuiltinWallpaperData, colors: [UInt32] ), progressiveSizes: [], immediateThumbnailData: nil, - hasVideo: false + hasVideo: false, + isPersonal: false ) ], videoThumbnails: [], diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index be62b20481b..7e7ac76508e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -51,6 +51,7 @@ public enum PresentationResourceKey: Int32 { case itemListCreateGroupIcon case itemListAddExceptionIcon case itemListAddPhoneIcon + case itemListAddPhotoIcon case itemListClearInputIcon case itemListStickerItemUnreadDot case itemListVerifiedPeerIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 9a1a21b42bd..9d009cdc176 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -169,6 +169,12 @@ public struct PresentationResourcesItemList { }) } + public static func addPhotoIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAddPhotoIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/SetAvatar"), color: theme.list.itemAccentColor) + }) + } + public static func itemListClearInputIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListClearInputIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.list.inputClearButtonColor) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 22def89dab8..3605d06c1cd 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -825,6 +825,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChanged(".")._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } } + case let .suggestedProfilePhoto(image): + if (image?.videoRepresentations.isEmpty ?? true) { + attributedString = NSAttributedString(string: strings.Notification_SuggestedProfilePhoto, font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Notification_SuggestedProfileVideo, font: titleFont, textColor: primaryTextColor) + } + case .attachMenuBotAllowed: + attributedString = NSAttributedString(string: strings.Notification_BotWriteAllowed, font: titleFont, textColor: primaryTextColor) case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 8ce31fa7fa1..9204942c02f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -358,6 +358,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", + "//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", @@ -369,7 +370,11 @@ swift_library( "//submodules/InviteLinksUI:InviteLinksUI", "//submodules/TelegramUI/Components/NotificationPeerExceptionController", "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/TelegramUI/Components/ChatInputNode", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/MediaPasteboardUI:MediaPasteboardUI", + "//submodules/DrawingUI:DrawingUI", + "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/BUILD b/submodules/TelegramUI/Components/ChatControllerInteraction/BUILD new file mode 100644 index 00000000000..684e3ca458d --- /dev/null +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatControllerInteraction", + module_name = "ChatControllerInteraction", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/TextSelectionNode:TextSelectionNode", + "//submodules/ContextUI:ContextUI", + "//submodules/ChatInterfaceState:ChatInterfaceState", + "//submodules/UndoUI:UndoUI", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TextFormat:TextFormat", + "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift similarity index 59% rename from submodules/TelegramUI/Sources/ChatControllerInteraction.swift rename to submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 11f2034f4ee..0f7a2930742 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -15,33 +15,28 @@ import UndoUI import TelegramPresentationData import ChatPresentationInterfaceState import TextFormat +import WallpaperBackgroundNode +import AnimationCache +import MultiAnimationRenderer -struct ChatInterfaceHighlightedState: Equatable { - let messageStableId: UInt32 +public struct ChatInterfaceHighlightedState: Equatable { + public let messageStableId: UInt32 - static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool { + public init(messageStableId: UInt32) { + self.messageStableId = messageStableId + } + + public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool { return lhs.messageStableId == rhs.messageStableId } } -struct ChatInterfaceStickerSettings: Equatable { - let loopAnimatedStickers: Bool +public struct ChatInterfacePollActionState: Equatable { + public var pollMessageIdsInProgress: [MessageId: [Data]] = [:] - public init(loopAnimatedStickers: Bool) { - self.loopAnimatedStickers = loopAnimatedStickers + public init(pollMessageIdsInProgress: [MessageId: [Data]] = [:]) { + self.pollMessageIdsInProgress = pollMessageIdsInProgress } - - public init(stickerSettings: StickerSettings) { - self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers - } - - static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool { - return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers - } -} - -struct ChatInterfacePollActionState: Equatable { - var pollMessageIdsInProgress: [MessageId: [Data]] = [:] } public enum ChatControllerInteractionSwipeAction { @@ -54,131 +49,153 @@ public enum ChatControllerInteractionReaction { case reaction(MessageReaction.Reaction) } -struct UnreadMessageRangeKey: Hashable { - var peerId: PeerId - var namespace: MessageId.Namespace +public struct UnreadMessageRangeKey: Hashable { + public var peerId: PeerId + public var namespace: MessageId.Namespace + + public init(peerId: PeerId, namespace: MessageId.Namespace) { + self.peerId = peerId + self.namespace = namespace + } +} + +public class ChatPresentationContext { + public weak var backgroundNode: WallpaperBackgroundNode? + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + + public init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) { + self.backgroundNode = backgroundNode + + self.animationCache = context.animationCache + self.animationRenderer = context.animationRenderer + } +} + +public protocol ChatMessageTransitionProtocol: ASDisplayNode { + } public final class ChatControllerInteraction { - enum OpenPeerSource { + public enum OpenPeerSource { case `default` case reaction case groupParticipant } // MARK: Nicegram Translate - let onTranslateButtonLongTap: () -> Void + public let onTranslateButtonLongTap: () -> Void // - let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool - let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void - let openPeerMention: (String) -> Void - let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void - let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void - let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void - let activateMessagePinch: (PinchSourceContainerNode) -> Void - let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void - let navigateToMessage: (MessageId, MessageId) -> Void - let navigateToMessageStandalone: (MessageId) -> Void - let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void - let tapMessage: ((Message) -> Void)? - let clickThroughMessage: () -> Void - let toggleMessagesSelection: ([MessageId], Bool) -> Void - let sendCurrentMessage: (Bool) -> Void - let sendMessage: (String) -> Void - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool - let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void - let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool - let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool - let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void - let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void - let activateSwitchInline: (PeerId?, String) -> Void - let openUrl: (String, Bool, Bool?, Message?) -> Void - let shareCurrentLocation: () -> Void - let shareAccountContact: () -> Void - let sendBotCommand: (MessageId?, String) -> Void - let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void - let openWallpaper: (Message) -> Void - let openTheme: (Message) -> Void - let openHashtag: (String?, String) -> Void - let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void - let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void - let openMessageShareMenu: (MessageId) -> Void - let presentController: (ViewController, Any?) -> Void - let presentControllerInCurrent: (ViewController, Any?) -> Void - let navigationController: () -> NavigationController? - let chatControllerNode: () -> ASDisplayNode? - let presentGlobalOverlayController: (ViewController, Any?) -> Void - let callPeer: (PeerId, Bool) -> Void - let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void - let openCheckoutOrReceipt: (MessageId) -> Void - let openSearch: () -> Void - let setupReply: (MessageId) -> Void - let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction - let navigateToFirstDateMessage: (Int32, Bool) -> Void - let requestRedeliveryOfFailedMessages: (MessageId) -> Void - let addContact: (String) -> Void - let rateCall: (Message, CallId, Bool) -> Void - let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void - let requestOpenMessagePollResults: (MessageId, MediaId) -> Void - let openAppStorePage: () -> Void - let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void - let seekToTimecode: (Message, Double, Bool) -> Void - let scheduleCurrentMessage: () -> Void - let sendScheduledMessagesNow: ([MessageId]) -> Void - let editScheduledMessagesTime: ([MessageId]) -> Void - let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void - let displayImportedMessageTooltip: (ASDisplayNode) -> Void - let displaySwipeToReplyHint: () -> Void - let dismissReplyMarkupMessage: (Message) -> Void - let openMessagePollResults: (MessageId, Data) -> Void - let openPollCreation: (Bool?) -> Void - let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void - let displayPsa: (String, ASDisplayNode) -> Void - let displayDiceTooltip: (TelegramMediaDice) -> Void - let animateDiceSuccess: (Bool, Bool) -> Void - let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void - let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void - let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void - let openMessageReplies: (MessageId, Bool, Bool) -> Void - let openReplyThreadOriginalMessage: (Message) -> Void - let openMessageStats: (MessageId) -> Void - let editMessageMedia: (MessageId, Bool) -> Void - let copyText: (String) -> Void - let displayUndo: (UndoOverlayContent) -> Void - let isAnimatingMessage: (UInt32) -> Bool - let getMessageTransitionNode: () -> ChatMessageTransitionNode? - let updateChoosingSticker: (Bool) -> Void - let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void - let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void - let openJoinLink: (String) -> Void - let openWebView: (String, String, Bool, Bool) -> Void - let activateAdAction: (EngineMessage.Id) -> Void + public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool + public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void + public let openPeerMention: (String) -> Void + public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void + public let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void + public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void + public let activateMessagePinch: (PinchSourceContainerNode) -> Void + public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void + public let navigateToMessage: (MessageId, MessageId) -> Void + public let navigateToMessageStandalone: (MessageId) -> Void + public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void + public let tapMessage: ((Message) -> Void)? + public let clickThroughMessage: () -> Void + public let toggleMessagesSelection: ([MessageId], Bool) -> Void + public let sendCurrentMessage: (Bool) -> Void + public let sendMessage: (String) -> Void + public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool + public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void + public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool + public let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool + public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void + public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void + public let activateSwitchInline: (PeerId?, String) -> Void + public let openUrl: (String, Bool, Bool?, Message?) -> Void + public let shareCurrentLocation: () -> Void + public let shareAccountContact: () -> Void + public let sendBotCommand: (MessageId?, String) -> Void + public let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void + public let openWallpaper: (Message) -> Void + public let openTheme: (Message) -> Void + public let openHashtag: (String?, String) -> Void + public let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void + public let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void + public let openMessageShareMenu: (MessageId) -> Void + public let presentController: (ViewController, Any?) -> Void + public let presentControllerInCurrent: (ViewController, Any?) -> Void + public let navigationController: () -> NavigationController? + public let chatControllerNode: () -> ASDisplayNode? + public let presentGlobalOverlayController: (ViewController, Any?) -> Void + public let callPeer: (PeerId, Bool) -> Void + public let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void + public let openCheckoutOrReceipt: (MessageId) -> Void + public let openSearch: () -> Void + public let setupReply: (MessageId) -> Void + public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction + public let navigateToFirstDateMessage: (Int32, Bool) -> Void + public let requestRedeliveryOfFailedMessages: (MessageId) -> Void + public let addContact: (String) -> Void + public let rateCall: (Message, CallId, Bool) -> Void + public let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void + public let requestOpenMessagePollResults: (MessageId, MediaId) -> Void + public let openAppStorePage: () -> Void + public let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void + public let seekToTimecode: (Message, Double, Bool) -> Void + public let scheduleCurrentMessage: () -> Void + public let sendScheduledMessagesNow: ([MessageId]) -> Void + public let editScheduledMessagesTime: ([MessageId]) -> Void + public let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void + public let displayImportedMessageTooltip: (ASDisplayNode) -> Void + public let displaySwipeToReplyHint: () -> Void + public let dismissReplyMarkupMessage: (Message) -> Void + public let openMessagePollResults: (MessageId, Data) -> Void + public let openPollCreation: (Bool?) -> Void + public let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void + public let displayPsa: (String, ASDisplayNode) -> Void + public let displayDiceTooltip: (TelegramMediaDice) -> Void + public let animateDiceSuccess: (Bool, Bool) -> Void + public let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void + public let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void + public let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void + public let openMessageReplies: (MessageId, Bool, Bool) -> Void + public let openReplyThreadOriginalMessage: (Message) -> Void + public let openMessageStats: (MessageId) -> Void + public let editMessageMedia: (MessageId, Bool) -> Void + public let copyText: (String) -> Void + public let displayUndo: (UndoOverlayContent) -> Void + public let isAnimatingMessage: (UInt32) -> Bool + public let getMessageTransitionNode: () -> ChatMessageTransitionProtocol? + public let updateChoosingSticker: (Bool) -> Void + public let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void + public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void + public let openJoinLink: (String) -> Void + public let openWebView: (String, String, Bool, Bool) -> Void + public let activateAdAction: (EngineMessage.Id) -> Void - let requestMessageUpdate: (MessageId, Bool) -> Void - let cancelInteractiveKeyboardGestures: () -> Void - let dismissTextInput: () -> Void - let scrollToMessageId: (MessageIndex) -> Void + public let requestMessageUpdate: (MessageId, Bool) -> Void + public let cancelInteractiveKeyboardGestures: () -> Void + public let dismissTextInput: () -> Void + public let scrollToMessageId: (MessageIndex) -> Void - var canPlayMedia: Bool = false - var hiddenMedia: [MessageId: [Media]] = [:] - var expandedTranslationMessageStableIds: Set = Set() - var selectionState: ChatInterfaceSelectionState? - var highlightedState: ChatInterfaceHighlightedState? - var contextHighlightedState: ChatInterfaceHighlightedState? - var automaticMediaDownloadSettings: MediaAutoDownloadSettings - var pollActionState: ChatInterfacePollActionState - var currentPollMessageWithTooltip: MessageId? - var currentPsaMessageWithTooltip: MessageId? - var stickerSettings: ChatInterfaceStickerSettings - var searchTextHighightState: (String, [MessageIndex])? - var unreadMessageRange: [UnreadMessageRangeKey: Range] = [:] - var seenOneTimeAnimatedMedia = Set() - var currentMessageWithLoadingReplyThread: MessageId? - var updatedPresentationData: (initial: PresentationData, signal: Signal)? - let presentationContext: ChatPresentationContext - var playNextOutgoingGift: Bool = false + public var canPlayMedia: Bool = false + public var hiddenMedia: [MessageId: [Media]] = [:] + public var expandedTranslationMessageStableIds: Set = Set() + public var selectionState: ChatInterfaceSelectionState? + public var highlightedState: ChatInterfaceHighlightedState? + public var contextHighlightedState: ChatInterfaceHighlightedState? + public var automaticMediaDownloadSettings: MediaAutoDownloadSettings + public var pollActionState: ChatInterfacePollActionState + public var currentPollMessageWithTooltip: MessageId? + public var currentPsaMessageWithTooltip: MessageId? + public var stickerSettings: ChatInterfaceStickerSettings + public var searchTextHighightState: (String, [MessageIndex])? + public var unreadMessageRange: [UnreadMessageRangeKey: Range] = [:] + public var seenOneTimeAnimatedMedia = Set() + public var currentMessageWithLoadingReplyThread: MessageId? + public var updatedPresentationData: (initial: PresentationData, signal: Signal)? + public let presentationContext: ChatPresentationContext + public var playNextOutgoingGift: Bool = false - init( + public init( // MARK: Nicegram Translate onTranslateButtonLongTap: @escaping () -> Void = {}, // @@ -259,7 +276,7 @@ public final class ChatControllerInteraction { copyText: @escaping (String) -> Void, displayUndo: @escaping (UndoOverlayContent) -> Void, isAnimatingMessage: @escaping (UInt32) -> Bool, - getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?, + getMessageTransitionNode: @escaping () -> ChatMessageTransitionProtocol?, updateChoosingSticker: @escaping (Bool) -> Void, commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void, openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD new file mode 100644 index 00000000000..5ec6a15c17e --- /dev/null +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatEntityKeyboardInputNode", + module_name = "ChatEntityKeyboardInputNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TextFormat:TextFormat", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode", + "//submodules/Components/PagerComponent:PagerComponent", + "//submodules/PremiumUI:PremiumUI", + "//submodules/UndoUI:UndoUI", + "//submodules/ContextUI:ContextUI", + "//submodules/GalleryUI:GalleryUI", + "//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramNotices:TelegramNotices", + "//submodules/StickerPeekUI:StickerPeekUI", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction", + "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift similarity index 74% rename from submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift rename to submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 337ede1e867..0af5969e773 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -12,7 +12,6 @@ import MultiAnimationRenderer import Postbox import TelegramCore import ComponentDisplayAdapters -import SettingsUI import TextFormat import PagerComponent import AppBundle @@ -25,17 +24,37 @@ import AttachmentTextInputPanelNode import TelegramPresentationData import TelegramNotices import StickerPeekUI +import ChatInputNode +import TelegramUIPreferences +import MultiplexedVideoNode +import ChatControllerInteraction +import FeaturedStickersScreen -final class EntityKeyboardGifContent: Equatable { - let hasRecentGifs: Bool - let component: GifPagerContentComponent +public struct ChatMediaInputPaneScrollState { + let absoluteOffset: CGFloat? + let relativeChange: CGFloat +} + +public final class ChatMediaInputGifPaneTrendingState { + public let files: [MultiplexedVideoNodeFile] + public let nextOffset: String? - init(hasRecentGifs: Bool, component: GifPagerContentComponent) { + public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) { + self.files = files + self.nextOffset = nextOffset + } +} + +public final class EntityKeyboardGifContent: Equatable { + public let hasRecentGifs: Bool + public let component: GifPagerContentComponent + + public init(hasRecentGifs: Bool, component: GifPagerContentComponent) { self.hasRecentGifs = hasRecentGifs self.component = component } - static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool { + public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool { if lhs.hasRecentGifs != rhs.hasRecentGifs { return false } @@ -46,14 +65,14 @@ final class EntityKeyboardGifContent: Equatable { } } -final class ChatEntityKeyboardInputNode: ChatInputNode { - struct InputData: Equatable { - var emoji: EmojiPagerContentComponent - var stickers: EmojiPagerContentComponent? - var gifs: EntityKeyboardGifContent? - var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] - - init( +public final class ChatEntityKeyboardInputNode: ChatInputNode { + public struct InputData: Equatable { + public var emoji: EmojiPagerContentComponent + public var stickers: EmojiPagerContentComponent? + public var gifs: EntityKeyboardGifContent? + public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] + + public init( emoji: EmojiPagerContentComponent, stickers: EmojiPagerContentComponent?, gifs: EntityKeyboardGifContent?, @@ -66,7 +85,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { + public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { let hasPremium: Signal if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { hasPremium = .single(true) @@ -83,10 +102,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let isPremiumDisabled = premiumConfiguration.isPremiumDisabled - + public static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer @@ -94,458 +110,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] - - struct PeerSpecificPackData: Equatable { - var info: StickerPackCollectionInfo - var items: [StickerPackItem] - var peer: EnginePeer - - static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { - if lhs.info.id != rhs.info.id { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.peer != rhs.peer { - return false - } - return true - } - } - - let peerSpecificPack: Signal - if let chatPeerId = chatPeerId { - peerSpecificPack = combineLatest( - context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) - ) - |> map { packData, peer -> PeerSpecificPackData? in - guard let peer = peer else { - return nil - } - - guard let (info, items) = packData.packInfo else { - return nil - } - - return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) - } - |> distinctUntilChanged - } else { - peerSpecificPack = .single(nil) - } - let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let stickerItems: Signal = combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), - ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), - context.account.viewTracker.featuredStickerPacks(), - context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), - ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), - peerSpecificPack - ) - |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in - struct ItemGroup { - var supergroupId: AnyHashable - var id: AnyHashable - var title: String - var subtitle: String? - var actionButtonTitle: String? - var isPremiumLocked: Bool - var isFeatured: Bool - var displayPremiumBadges: Bool - var headerItem: EntityKeyboardAnimationData? - var items: [EmojiPagerContentComponent.Item] - } - var itemGroups: [ItemGroup] = [] - var itemGroupIndexById: [AnyHashable: Int] = [:] - - var savedStickers: OrderedItemListView? - var recentStickers: OrderedItemListView? - var cloudPremiumStickers: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { - recentStickers = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { - savedStickers = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers { - cloudPremiumStickers = orderedView - } - } - - var installedCollectionIds = Set() - for (id, _, _) in view.collectionInfos { - installedCollectionIds.insert(id) - } - - let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? []) - let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id)) - - if dismissedTrendingStickerPacksSet != featuredStickerPacksSet { - let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) - for featuredStickerPack in featuredStickerPacks { - if installedCollectionIds.contains(featuredStickerPack.info.id) { - continue - } - - guard let item = featuredStickerPack.topItems.first else { - continue - } - - let animationData: EntityKeyboardAnimationData - - if let thumbnail = featuredStickerPack.info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - animationData = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(featuredStickerPack.info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, - isReaction: false - ) - } else { - animationData = EntityKeyboardAnimationData(file: item.file) - } - - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let supergroupId = "featuredTop" - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false - let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks - - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let savedStickers = savedStickers { - for item in savedStickers.items { - guard let item = item.contents.get(SavedStickerItem.self) else { - continue - } - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "saved" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - if let recentStickers = recentStickers { - for item in recentStickers.items { - guard let item = item.contents.get(RecentMediaItem.self) else { - continue - } - if isPremiumDisabled && item.media.isPremiumSticker { - continue - } - - let animationData = EntityKeyboardAnimationData(file: item.media) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.media, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "recent" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - var premiumStickers: [StickerPackItem] = [] - if hasPremium { - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - - if item.file.isPremiumSticker { - premiumStickers.append(item) - } - } - - if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { - premiumStickers.append(contentsOf: cloudPremiumStickers.items.compactMap { item -> StickerPackItem? in guard let item = item.contents.get(RecentMediaItem.self) else { - return nil - } - return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.media, indexKeys: []) - }) - } - } - - if !premiumStickers.isEmpty { - var processedIds = Set() - for item in premiumStickers { - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - if processedIds.contains(item.file.fileId) { - continue - } - processedIds.insert(item.file.fileId) - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "premium" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitlePremiumStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - var avatarPeer: EnginePeer? - if let peerSpecificPack = peerSpecificPack { - avatarPeer = peerSpecificPack.peer - - var processedIds = Set() - for item in peerSpecificPack.items { - if isPremiumDisabled && item.file.isPremiumSticker { - continue - } - if processedIds.contains(item.file.fileId) { - continue - } - processedIds.insert(item.file.fileId) - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let groupId = "peerSpecific" - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) - } - } - } - - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - let groupId = entry.index.collectionId - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - var title = "" - var headerItem: EntityKeyboardAnimationData? - inner: for (id, info, _) in view.collectionInfos { - if id == groupId, let info = info as? StickerPackCollectionInfo { - title = info.title - - if let thumbnail = info.thumbnail { - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - break inner - } - } - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem])) - } - } - - for featuredStickerPack in featuredStickerPacks { - if installedCollectionIds.contains(featuredStickerPack.info.id) { - continue - } - - for item in featuredStickerPack.topItems { - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, - icon: .none, - accentTint: false - ) - - let supergroupId = featuredStickerPack.info.id - let groupId: AnyHashable = supergroupId - let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium - if isPremiumLocked && isPremiumDisabled { - continue - } - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - - let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count)) - var headerItem: EntityKeyboardAnimationData? - - if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { - headerItem = EntityKeyboardAnimationData(file: file.file) - } else if let thumbnail = featuredStickerPack.info.thumbnail { - let info = featuredStickerPack.info - let type: EntityKeyboardAnimationData.ItemType - if item.file.isAnimatedSticker { - type = .lottie - } else if item.file.isVideoEmoji || item.file.isVideoSticker { - type = .video - } else { - type = .still - } - - headerItem = EntityKeyboardAnimationData( - id: .stickerPackThumbnail(info.id), - type: type, - resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), - dimensions: thumbnail.dimensions.cgSize, - immediateThumbnailData: info.immediateThumbnailData, - isReaction: false - ) - } - - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem])) - } - } - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return EmojiPagerContentComponent( - id: "stickers", - context: context, - avatarPeer: avatarPeer, - animationCache: animationCache, - animationRenderer: animationRenderer, - inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), - itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var hasClear = false - var isEmbedded = false - if group.id == AnyHashable("recent") { - hasClear = true - } else if group.id == AnyHashable("featuredTop") { - hasClear = true - isEmbedded = true - } - - return EmojiPagerContentComponent.ItemGroup( - supergroupId: group.supergroupId, - groupId: group.id, - title: group.title, - subtitle: group.subtitle, - actionButtonTitle: group.actionButtonTitle, - isFeatured: group.isFeatured, - isPremiumLocked: group.isPremiumLocked, - isEmbedded: isEmbedded, - hasClear: hasClear, - collapsedLineCount: nil, - displayPremiumBadges: group.displayPremiumBadges, - headerItem: group.headerItem, - items: group.items - ) - }, - itemLayoutType: .detailed, - itemContentUniqueId: nil, - warpContentsOnEdges: false, - displaySearchWithPlaceholder: presentationData.strings.StickersSearch_SearchStickersPlaceholder, - searchInitiallyHidden: false, - searchIsPlaceholderOnly: true, - emptySearchResults: nil, - enableLongPress: false, - selectedItems: Set() - ) - } + let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: true, hasTrending: true, forceHasPremium: false) let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in @@ -604,7 +172,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: false, loadMoreToken: nil, displaySearchWithPlaceholder: nil, - searchInitiallyHidden: false + searchInitiallyHidden: true ) )) @@ -692,12 +260,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } + fileprivate var clipContentToTopPanel: Bool = false + var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? - override var externalTopPanelContainer: UIView? { + public override var externalTopPanelContainer: UIView? { return self.externalTopPanelContainerImpl } - var switchToTextInput: (() -> Void)? + public var switchToTextInput: (() -> Void)? private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? @@ -712,7 +282,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - var canSwitchToTextInputAutomatically: Bool { + public var canSwitchToTextInputAutomatically: Bool { if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { return false @@ -776,7 +346,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: false, loadMoreToken: nil, displaySearchWithPlaceholder: presentationData.strings.GifSearch_SearchGifPlaceholder, - searchInitiallyHidden: false + searchInitiallyHidden: true ) ) } @@ -807,7 +377,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: isLoading, loadMoreToken: nil, displaySearchWithPlaceholder: nil, - searchInitiallyHidden: false + searchInitiallyHidden: true ) ) } @@ -840,7 +410,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: isLoading, loadMoreToken: loadMoreToken, displaySearchWithPlaceholder: nil, - searchInitiallyHidden: false + searchInitiallyHidden: true ) ) } @@ -921,7 +491,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { isLoading: isLoading, loadMoreToken: loadMoreToken, displaySearchWithPlaceholder: nil, - searchInitiallyHidden: false + searchInitiallyHidden: true ) ) } @@ -948,7 +518,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private weak var currentUndoOverlayController: UndoOverlayController? // MARK: Nicegram OpenGifsShortcut, defaultTab param added - init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) { + public init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) { // MARK: Nicegram OpenGifsShortcut self.defaultTab = defaultTab // @@ -975,7 +545,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in - guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { + guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return } @@ -984,7 +554,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? @@ -1239,7 +809,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -1269,7 +839,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -1282,8 +852,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false) - ) + tintMode: .none + )) } return [EmojiPagerContentComponent.ItemGroup( @@ -1314,20 +884,26 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { })) } }, + updateScrollingToItemGroup: { + }, chatPeerId: chatPeerId, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, - useOpaqueTheme: false + useOpaqueTheme: false, + hideBackground: false ) var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? if let controllerInteraction = controllerInteraction { stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: self.context, - controllerInteraction: controllerInteraction, - chatPeerId: chatPeerId + interaction: EmojiContentPeekBehaviorImpl.Interaction(sendSticker: controllerInteraction.sendSticker, presentController: controllerInteraction.presentController, presentGlobalOverlayController: controllerInteraction.presentGlobalOverlayController, navigationController: controllerInteraction.navigationController), + chatPeerId: chatPeerId, + present: { c, a in + controllerInteraction.presentGlobalOverlayController(c, a) + } ) } self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( @@ -1393,7 +969,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { guard let controllerInteraction = controllerInteraction else { return } - let controller = installedStickerPacksController(context: context, mode: .modal) + let controller = context.sharedContext.makeInstalledStickerPacksController(context: context, mode: .modal) controller.navigationPresentation = .modal controllerInteraction.navigationController()?.pushViewController(controller) }, @@ -1524,12 +1100,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }, updateSearchQuery: { _, _ in }, + updateScrollingToItemGroup: { + }, chatPeerId: chatPeerId, peekBehavior: stickerPeekBehavior, customLayout: nil, externalBackground: nil, externalExpansionView: nil, - useOpaqueTheme: false + useOpaqueTheme: false, + hideBackground: false ) self.inputDataDisposable = (combineLatest(queue: .mainQueue(), @@ -1544,11 +1123,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var inputData = inputData inputData.gifs = gifs + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let emojiSearchResult = emojiSearchResult { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( - text: "No emoji found", //strongSelf.presentationData.strings.EmojiSearch_SearchStatusesEmptyResult, + text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, iconFile: nil ) } @@ -1688,7 +1269,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - func markInputCollapsed() { + public func markInputCollapsed() { self.isMarkInputCollapsed = true } @@ -1700,14 +1281,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - func simulateUpdateLayout(isVisible: Bool) { + public func simulateUpdateLayout(isVisible: Bool) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _, isExpanded) = self.currentState else { return } let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { + public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) let innerTransition: Transition @@ -1779,11 +1360,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { topPanelInsets: UIEdgeInsets(), emojiContent: self.currentInputData.emoji, stickerContent: stickerContent, + maskContent: nil, gifContent: gifContent?.component, hasRecentGifs: gifContent?.hasRecentGifs ?? false, availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies, defaultToEmojiTab: self.defaultToEmojiTab, externalTopPanelContainer: self.externalTopPanelContainerImpl, + externalBottomPanelContainer: nil, + displayTopPanelBackground: false, topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in guard let strongSelf = self else { return @@ -1855,7 +1439,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { hiddenInputHeight: hiddenInputHeight, inputHeight: inputHeight, displayBottomPanel: true, - isExpanded: isExpanded && !self.isEmojiSearchActive + isExpanded: isExpanded && !self.isEmojiSearchActive, + clipContentToTopPanel: self.clipContentToTopPanel )), environment: {}, containerSize: CGSize(width: width, height: expandedHeight) @@ -1963,6 +1548,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { namespace = Namespaces.ItemCollection.CloudStickerPacks case .emoji: namespace = Namespaces.ItemCollection.CloudEmojiPacks + case .masks: + namespace = Namespaces.ItemCollection.CloudMaskPacks } self.stableReorderableGroupOrder.removeValue(forKey: category) @@ -2146,7 +1733,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext public var insertText: ((NSAttributedString) -> Void)? @@ -2159,10 +1746,12 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - init( + public init( context: AccountContext, isDark: Bool, - areCustomEmojiEnabled: Bool + areCustomEmojiEnabled: Bool, + hideBackground: Bool = false, + forceHasPremium: Bool = false ) { self.context = context @@ -2174,15 +1763,21 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) } - //super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default) - super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))) + super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default) +// super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))) self.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.clipsToBounds = true let inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in - let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + let hasPremium: Signal + if forceHasPremium { + hasPremium = .single(true) + } else { + hasPremium = ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue + } + let _ = hasPremium.start(next: { hasPremium in guard let strongSelf = self else { return } @@ -2192,7 +1787,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { @@ -2279,21 +1874,24 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV navigationController: { return nil }, - requestUpdate: { _ in + requestUpdate: { _ in }, updateSearchQuery: { _, _ in }, + updateScrollingToItemGroup: { + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, - useOpaqueTheme: false + useOpaqueTheme: false, + hideBackground: hideBackground ) let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in + let _ = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2308,7 +1906,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: EmojiPagerContentComponent.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, forceHasPremium: forceHasPremium) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, @@ -2322,6 +1920,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV chatPeerId: nil ) self.inputNode = inputNode + inputNode.clipContentToTopPanel = hideBackground inputNode.emojiInputInteraction = inputInteraction inputNode.externalTopPanelContainerImpl = nil inputNode.switchToTextInput = { [weak self] in @@ -2335,7 +1934,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV fatalError("init(coder:) has not been implemented") } - override func layoutSubviews() { + public override func layoutSubviews() { super.layoutSubviews() guard let inputNode = self.inputNode else { @@ -2397,21 +1996,37 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV } } -private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { +public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { + public class Interaction { + public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool + public let presentController: (ViewController, Any?) -> Void + public let presentGlobalOverlayController: (ViewController, Any?) -> Void + public let navigationController: () -> NavigationController? + + public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?) { + self.sendSticker = sendSticker + self.presentController = presentController + self.presentGlobalOverlayController = presentGlobalOverlayController + self.navigationController = navigationController + } + } + private let context: AccountContext - private let controllerInteraction: ChatControllerInteraction + private let interaction: Interaction? private let chatPeerId: EnginePeer.Id? + private let present: (ViewController, Any?) -> Void private var peekRecognizer: PeekControllerGestureRecognizer? private weak var peekController: PeekController? - init(context: AccountContext, controllerInteraction: ChatControllerInteraction, chatPeerId: EnginePeer.Id?) { + public init(context: AccountContext, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) { self.context = context - self.controllerInteraction = controllerInteraction + self.interaction = interaction self.chatPeerId = chatPeerId + self.present = present } - func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) { + public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?) { if self.peekRecognizer == nil { let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self, weak view] point in guard let strongSelf = self else { @@ -2444,123 +2059,116 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return nil } var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let isLocked = file.isPremiumSticker && !hasPremium - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in - guard let strongSelf = self else { - return + if let interaction = strongSelf.interaction { + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in + let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) } - let _ = strongSelf.controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) - } - - if let chatPeerId = strongSelf.chatPeerId { - if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { - menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + + if let chatPeerId = strongSelf.chatPeerId, !isLocked { + if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + if let strongSelf = self, let peekController = strongSelf.peekController { + if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { + sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) + } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { + sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) + } + } + f(.default) + }))) + } + + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) }))) } - - menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - if let strongSelf = self, let peekController = strongSelf.peekController { - if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) - } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) - } - } - f(.default) - }))) - } - - menuItems.append( - .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) - |> deliverOnMainQueue).start(next: { result in + + menuItems.append( + .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) + |> deliverOnMainQueue).start(next: { result in + switch result { + case .generic: + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) + case let .limitExceeded(limit, premiumLimit): + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let text: String + if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { + text = presentationData.strings.Premium_MaxFavedStickersFinalText + } else { + text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string + } + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in + if case .info = action { + let controller = PremiumIntroScreen(context: context, source: .savedStickers) + interaction.navigationController()?.pushViewController(controller) + return true + } + return false + }), nil) + } + }) + })) + ) + menuItems.append( + .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + guard let strongSelf = self else { return } - switch result { - case .generic: - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) - case let .limitExceeded(limit, premiumLimit): - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - let text: String - if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { - text = presentationData.strings.Premium_MaxFavedStickersFinalText - } else { - text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string - } - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in - guard let strongSelf = self else { - return false - } - if case .info = action { - let controller = PremiumIntroScreen(context: context, source: .savedStickers) - strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) - return true - } - return false - }), nil) - } - }) - })) - ) - menuItems.append( - .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) - - guard let strongSelf = self else { - return - } - loop: for attribute in file.attributes { - switch attribute { - case let .CustomEmoji(_, _, packReference), let .Sticker(_, packReference, _): - if let packReference = packReference { - let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in - sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) - return true - }) - - strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) - strongSelf.controllerInteraction.presentController(controller, nil) + switch attribute { + case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in + sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) + return true + }) + + interaction.navigationController()?.view.window?.endEditing(true) + interaction.presentController(controller, nil) + } + break loop + default: + break } - break loop - default: - break } - } - })) - ) + })) + ) + } guard let view = view else { return nil } - - return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { - guard let strongSelf = self else { + + return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: { + guard let strongSelf = self, let interaction = strongSelf.interaction else { return } let controller = PremiumIntroScreen(context: context, source: .stickers) - strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) + interaction.navigationController()?.pushViewController(controller) })) } }, present: { [weak self] content, sourceView, sourceRect in @@ -2578,7 +2186,7 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self?.simulateUpdateLayout(isVisible: !visible) }*/ strongSelf.peekController = controller - strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil) + strongSelf.present(controller, nil) return controller }, updateContent: { [weak self] content in guard let strongSelf = self else { @@ -2595,3 +2203,112 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { } } } + +public class PaneGifSearchForQueryResult { + public let files: [MultiplexedVideoNodeFile] + public let nextOffset: String? + public let isComplete: Bool + public let isStale: Bool + + public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) { + self.files = files + self.nextOffset = nextOffset + self.isComplete = isComplete + self.isStale = isStale + } +} + +public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { + let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) + |> mapToSignal { searchBots -> Signal in + let botName = searchBots.gifBotUsername ?? "gif" + return context.engine.peers.resolvePeerByName(name: botName) + } + |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in + if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { + let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1) + |> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in + return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false) + } + + let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> + if delayRequest { + maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) + } else { + maybeDelayedContextResults = results + } + + return maybeDelayedContextResults + } else { + return .single((nil, true, false)) + } + } + return contextBot + |> mapToSignal { result -> Signal in + if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { + let results = collection.results + var references: [MultiplexedVideoNodeFile] = [] + for result in results { + switch result { + case let .externalReference(externalReference): + var imageResource: TelegramMediaResource? + var thumbnailResource: TelegramMediaResource? + var thumbnailIsVideo: Bool = false + var uniqueId: Int64? + if let content = externalReference.content { + imageResource = content.resource + if let resource = content.resource as? WebFileReferenceMediaResource { + uniqueId = Int64(HashFunctions.murMurHash32(resource.url)) + } + } + if let thumbnail = externalReference.thumbnail { + thumbnailResource = thumbnail.resource + if thumbnail.mimeType.hasPrefix("video/") { + thumbnailIsVideo = true + } + } + + if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions { + var previews: [TelegramMediaImageRepresentation] = [] + var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] + if let thumbnailResource = thumbnailResource { + if thumbnailIsVideo { + videoThumbnails.append(TelegramMediaFile.VideoThumbnail( + dimensions: dimensions, + resource: thumbnailResource + )) + } else { + previews.append(TelegramMediaImageRepresentation( + dimensions: dimensions, + resource: thumbnailResource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + )) + } + } + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) + references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) + } + case let .internalReference(internalReference): + if let file = internalReference.file { + references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) + } + } + } + return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2)) + } else if incompleteResults { + return .single(nil) + } else { + return .complete() + } + } + |> deliverOnMainQueue + |> beforeStarted { + updateActivity?(true) + } + |> afterCompleted { + updateActivity?(false) + } +} diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/GifPaneSearchContentNode.swift similarity index 65% rename from submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift rename to submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/GifPaneSearchContentNode.swift index 30db8de0cbf..cb49fd3cc74 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/GifPaneSearchContentNode.swift @@ -7,116 +7,10 @@ import Postbox import TelegramCore import TelegramPresentationData import AccountContext -import WebSearchUI import AppBundle - -class PaneGifSearchForQueryResult { - let files: [MultiplexedVideoNodeFile] - let nextOffset: String? - let isComplete: Bool - let isStale: Bool - - init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) { - self.files = files - self.nextOffset = nextOffset - self.isComplete = isComplete - self.isStale = isStale - } -} - -func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { - let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> mapToSignal { searchBots -> Signal in - let botName = searchBots.gifBotUsername ?? "gif" - return context.engine.peers.resolvePeerByName(name: botName) - } - |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in - if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let results = requestContextResults(context: context, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1) - |> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in - return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false) - } - - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> - if delayRequest { - maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) - } else { - maybeDelayedContextResults = results - } - - return maybeDelayedContextResults - } else { - return .single((nil, true, false)) - } - } - return contextBot - |> mapToSignal { result -> Signal in - if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { - let results = collection.results - var references: [MultiplexedVideoNodeFile] = [] - for result in results { - switch result { - case let .externalReference(externalReference): - var imageResource: TelegramMediaResource? - var thumbnailResource: TelegramMediaResource? - var thumbnailIsVideo: Bool = false - var uniqueId: Int64? - if let content = externalReference.content { - imageResource = content.resource - if let resource = content.resource as? WebFileReferenceMediaResource { - uniqueId = Int64(HashFunctions.murMurHash32(resource.url)) - } - } - if let thumbnail = externalReference.thumbnail { - thumbnailResource = thumbnail.resource - if thumbnail.mimeType.hasPrefix("video/") { - thumbnailIsVideo = true - } - } - - if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions { - var previews: [TelegramMediaImageRepresentation] = [] - var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] - if let thumbnailResource = thumbnailResource { - if thumbnailIsVideo { - videoThumbnails.append(TelegramMediaFile.VideoThumbnail( - dimensions: dimensions, - resource: thumbnailResource - )) - } else { - previews.append(TelegramMediaImageRepresentation( - dimensions: dimensions, - resource: thumbnailResource, - progressiveSizes: [], - immediateThumbnailData: nil, - hasVideo: false - )) - } - } - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) - references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) - } - case let .internalReference(internalReference): - if let file = internalReference.file { - references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) - } - } - } - return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2)) - } else if incompleteResults { - return .single(nil) - } else { - return .complete() - } - } - |> deliverOnMainQueue - |> beforeStarted { - updateActivity?(true) - } - |> afterCompleted { - updateActivity?(false) - } -} +import ChatControllerInteraction +import MultiplexedVideoNode +import ChatPresentationInterfaceState final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private let context: AccountContext diff --git a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift similarity index 99% rename from submodules/TelegramUI/Sources/PaneSearchBarNode.swift rename to submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift index 490394469a8..64754e73889 100644 --- a/submodules/TelegramUI/Sources/PaneSearchBarNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift @@ -6,6 +6,7 @@ import Display import TelegramPresentationData import ActivityIndicator import AppBundle +import FeaturedStickersScreen private func generateLoupeIcon(color: UIColor) -> UIImage? { return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color) diff --git a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift similarity index 83% rename from submodules/TelegramUI/Sources/PaneSearchContainerNode.swift rename to submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift index 13244d5c08f..c645036b579 100644 --- a/submodules/TelegramUI/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift @@ -9,10 +9,13 @@ import TelegramPresentationData import AccountContext import ChatPresentationInterfaceState import EntityKeyboard +import ChatControllerInteraction +import MultiplexedVideoNode +import FeaturedStickersScreen private let searchBarHeight: CGFloat = 52.0 -protocol PaneSearchContentNode { +public protocol PaneSearchContentNode { var ready: Signal { get } var deactivateSearchBar: (() -> Void)? { get set } var updateActivity: ((Bool) -> Void)? { get set } @@ -28,7 +31,7 @@ protocol PaneSearchContentNode { func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? } -final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { +public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { private let context: AccountContext private let mode: ChatMediaInputSearchMode public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode @@ -40,15 +43,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { private var validLayout: CGSize? - var onCancel: (() -> Void)? + public var onCancel: (() -> Void)? - var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? - var ready: Signal { + public var ready: Signal { return self.contentNode.ready } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise, cancel: @escaping () -> Void) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise, cancel: @escaping () -> Void) { self.context = context self.mode = mode self.controllerInteraction = controllerInteraction @@ -102,7 +105,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { } } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.contentNode.updateThemeAndStrings(theme: theme, strings: strings) self.searchBar.updateThemeAndStrings(theme: theme, strings: strings) @@ -117,15 +120,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) } - func updateQuery(_ query: String) { + public func updateQuery(_ query: String) { self.searchBar.updateQuery(query) } - func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { + public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight)) } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { self.validLayout = size transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -138,11 +141,11 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) } - func deactivate() { + public func deactivate() { self.searchBar.deactivate(clear: true) } - func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y if let placeholder = placeholder { let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) @@ -170,7 +173,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode { } } - func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { if case let .animated(duration, curve) = transition { if let size = self.validLayout { let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift similarity index 97% rename from submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift rename to submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 8430ee3fda1..78aa4ab462a 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -16,20 +16,10 @@ import Emoji import AppBundle import OverlayStatusController import UndoUI - -final class StickerPaneSearchInteraction { - let open: (StickerPackCollectionInfo) -> Void - let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void - let sendSticker: (FileMediaReference, UIView, CGRect) -> Void - let getItemIsPreviewed: (StickerPackItem) -> Bool - - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { - self.open = open - self.install = install - self.sendSticker = sendSticker - self.getItemIsPreviewed = getItemIsPreviewed - } -} +import ChatControllerInteraction +import FeaturedStickersScreen +import ChatPresentationInterfaceState +import FeaturedStickersScreen private enum StickerSearchEntryId: Equatable, Hashable { case sticker(String?, Int64) diff --git a/submodules/TelegramUI/Components/ChatInputNode/BUILD b/submodules/TelegramUI/Components/ChatInputNode/BUILD new file mode 100644 index 00000000000..1b45fe19a37 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatInputNode/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatInputNode", + module_name = "ChatInputNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ChatInputNode/Sources/ChatInputNode.swift b/submodules/TelegramUI/Components/ChatInputNode/Sources/ChatInputNode.swift new file mode 100644 index 00000000000..95f9bf353ad --- /dev/null +++ b/submodules/TelegramUI/Components/ChatInputNode/Sources/ChatInputNode.swift @@ -0,0 +1,34 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import ChatPresentationInterfaceState + +open class ChatInputNode: ASDisplayNode { + public var interfaceInteraction: ChatPanelInterfaceInteraction? + open var ready: Signal { + return .single(Void()) + } + + open var externalTopPanelContainer: UIView? { + return nil + } + + public var topBackgroundExtension: CGFloat = 0.0 + public var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? + + public var hideInput: Bool = false + public var adjustLayoutForHiddenInput: Bool = false + public var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)? + + public var followsDefaultHeight: Bool = false + + open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { + + } + + open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { + return (0.0, 0.0) + } +} diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index be0b8254e00..0c53822d396 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -471,7 +471,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { switch titleContent { case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _, customMessageCount): if let customMessageCount = customMessageCount, customMessageCount != 0 { - let string = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + let string = NSAttributedString(string: self.strings.Conversation_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let peer = peerViewMainPeer(peerView) { let servicePeer = isServicePeer(peer) diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 1f962744bf2..64b86c15f39 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -420,6 +420,7 @@ public final class EmojiStatusComponent: Component { } animationLayer = InlineStickerItemLayer( context: component.context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), file: emojiFile, @@ -442,7 +443,10 @@ public final class EmojiStatusComponent: Component { var accentTint = false if let _ = emojiThemeColor { for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + accentTint = true + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { @@ -456,8 +460,10 @@ public final class EmojiStatusComponent: Component { } if accentTint { animationLayer.contentTintColor = emojiThemeColor + animationLayer.dynamicColor = emojiThemeColor } else { animationLayer.contentTintColor = nil + animationLayer.dynamicColor = nil } animationLayer.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index dc341575158..0917647e6ba 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -39,7 +39,7 @@ private func randomGenericReactionEffect(context: AccountContext) -> Signal filter(\.complete) |> take(1)).start(next: { data in @@ -165,11 +165,14 @@ public final class EmojiStatusSelectionComponent: Component { topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), emojiContent: component.emojiContent, stickerContent: nil, + maskContent: nil, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: true, externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: true, topPanelExtensionUpdated: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { [weak self] hideTopPanel, transition in @@ -186,7 +189,8 @@ public final class EmojiStatusSelectionComponent: Component { hiddenInputHeight: 0.0, inputHeight: 0.0, displayBottomPanel: false, - isExpanded: false + isExpanded: false, + clipContentToTopPanel: false )), environment: {}, containerSize: availableSize @@ -329,7 +333,7 @@ public final class EmojiStatusSelectionController: ViewController { for item in featuredEmojiPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if filterList.contains(alt) { filteredFiles.append(item.file) } @@ -487,7 +491,7 @@ public final class EmojiStatusSelectionController: ViewController { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] { result.append((alt, item.file, keyword)) @@ -516,7 +520,7 @@ public final class EmojiStatusSelectionController: ViewController { content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, - accentTint: false + tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) } @@ -550,12 +554,15 @@ public final class EmojiStatusSelectionController: ViewController { })) } }, + updateScrollingToItemGroup: { + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, - useOpaqueTheme: true + useOpaqueTheme: true, + hideBackground: false ) strongSelf.refreshLayout(transition: .immediate) @@ -645,7 +652,10 @@ public final class EmojiStatusSelectionController: ViewController { } else if let itemFile = item.itemFile { var useCleanEffect = false for attribute in itemFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + useCleanEffect = true + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { @@ -684,6 +694,7 @@ public final class EmojiStatusSelectionController: ViewController { for animationLayer in allLayers { let baseItemLayer = InlineStickerItemLayer( context: self.context, + userLocation: .other, attemptSynchronousLoad: false, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: itemFile.fileId.id, file: itemFile), file: item.itemFile, @@ -692,8 +703,13 @@ public final class EmojiStatusSelectionController: ViewController { placeholderColor: UIColor(white: 0.0, alpha: 0.0), pointSize: CGSize(width: 32.0, height: 32.0) ) - if item.accentTint { + switch item.tintMode { + case .accent: baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor + case .primary: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor + case .none: + break } if let sublayers = animationLayer.sublayers { @@ -1001,7 +1017,7 @@ public final class EmojiStatusSelectionController: ViewController { if let itemFile = previewItem.item.itemFile { attributeLoop: for attribute in itemFile.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): emojiString = alt break attributeLoop default: @@ -1165,7 +1181,7 @@ public final class EmojiStatusSelectionController: ViewController { if let itemFile = item.itemFile { attributeLoop: for attribute in itemFile.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): emojiString = alt break attributeLoop default: diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift index 7f928927f1e..cf1c08aa50c 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -59,7 +59,7 @@ public final class EmojiSuggestionsComponent: Component { } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) { if !item.file.isPremiumEmoji || hasPremium { if !existingIds.contains(item.file.fileId) { @@ -78,7 +78,7 @@ public final class EmojiSuggestionsComponent: Component { for item in featuredPack.topItems { for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query || (!normalizedQuery.isEmpty && alt == normalizedQuery) { if !item.file.isPremiumEmoji || hasPremium { if !existingIds.contains(item.file.fileId) { @@ -107,6 +107,7 @@ public final class EmojiSuggestionsComponent: Component { public init( context: AccountContext, + userLocation: MediaResourceUserLocation, theme: PresentationTheme, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, @@ -283,6 +284,7 @@ public final class EmojiSuggestionsComponent: Component { } else { itemLayer = InlineStickerItemLayer( context: component.context, + userLocation: .other, attemptSynchronousLoad: synchronousLoad, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: item.fileId.id, file: item), file: item, diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index adcf94e737a..3676f520a05 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -96,7 +96,7 @@ public extension AnimationCacheAnimationType { } } -public func animationCacheFetchFile(context: AccountContext, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool) -> (AnimationCacheFetchOptions) -> Disposable { +public func animationCacheFetchFile(context: AccountContext, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resource: MediaResourceReference, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable { return { options in let source = AnimatedStickerResourceSource(account: context.account, resource: resource.resource, fitzModifier: nil, isVideo: false) @@ -107,19 +107,19 @@ public func animationCacheFetchFile(context: AccountContext, resource: MediaReso switch type { case .video: - cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly) + cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .lottie: guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { options.writer.finish() return } - cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly) + cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor) case .still: - cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer) + cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor) } }) - let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resource).start() + let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resource).start() return ActionDisposable { dataDisposable.dispose() @@ -142,6 +142,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } private let context: AccountContext + private let userLocation: MediaResourceUserLocation private let emoji: ChatTextInputTextCustomEmojiAttribute private let cache: AnimationCache private let renderer: MultiAnimationRenderer @@ -153,6 +154,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private let pixelSize: CGSize private var isDisplayingPlaceholder: Bool = false + private var didProcessTintColor: Bool = false public private(set) var file: TelegramMediaFile? private var infoDisposable: Disposable? @@ -168,6 +170,14 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public var dynamicColor: UIColor? { + didSet { + if self.dynamicColor != oldValue { + self.updateTintColor() + } + } + } + private var currentLoopCount: Int = 0 private var isInHierarchyValue: Bool = false @@ -179,13 +189,15 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } - public init(context: AccountContext, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, loopCount: Int? = nil) { + public init(context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { self.context = context + self.userLocation = userLocation self.emoji = emoji self.cache = cache self.renderer = renderer self.unique = unique self.placeholderColor = placeholderColor + self.dynamicColor = dynamicColor self.loopCount = loopCount let scale = min(2.0, UIScreenScale) @@ -239,7 +251,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private func updateTintColor() { if !self.isDisplayingPlaceholder { - self.layerTintColor = self.contentTintColor?.cgColor + var customColor = self.contentTintColor + if let file = self.file { + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + customColor = self.dynamicColor + } + } + } + } + + self.layerTintColor = customColor?.cgColor } else { self.layerTintColor = nil } @@ -311,10 +334,17 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.loadAnimation() } else { + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + let pointSize = self.pointSize let placeholderColor = self.placeholderColor let isThumbnailCancelled = Atomic(value: false) - self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true), completion: { [weak self] result, isFinal in + self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, userLocation: self.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { MultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) @@ -350,11 +380,18 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return } + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + let context = self.context if file.isAnimatedSticker || file.isVideoEmoji { let keyframeOnly = self.pixelSize.width >= 120.0 - self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly)) + self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: self.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) } else { self.disposable = renderer.add(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, unique: self.unique, size: self.pixelSize, fetch: { options in let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in @@ -362,10 +399,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return } - cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer) + cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil) }) - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: self.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start() return ActionDisposable { dataDisposable.dispose() @@ -405,6 +442,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { + if !self.didProcessTintColor { + //self.didProcessTintColor = true + self.updateTintColor() + } self.contents = contents } @@ -420,8 +461,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { public final class EmojiTextAttachmentView: UIView { private let contentLayer: InlineStickerItemLayer - public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { - self.contentLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize) + public init(context: AccountContext, userLocation: MediaResourceUserLocation, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, pointSize: CGSize) { + self.contentLayer = InlineStickerItemLayer(context: context, userLocation: userLocation, attemptSynchronousLoad: true, emoji: emoji, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: pointSize) super.init(frame: CGRect()) @@ -433,9 +474,79 @@ public final class EmojiTextAttachmentView: UIView { fatalError("init(coder:) has not been implemented") } + public func updateTextColor(_ textColor: UIColor) { + self.contentLayer.dynamicColor = textColor + } + override public func layoutSubviews() { super.layoutSubviews() self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) } } + +public final class CustomEmojiContainerView: UIView { + private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? + + private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] + + public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { + self.emojiViewProvider = emojiViewProvider + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + var nextIndexById: [Int64: Int] = [:] + + var validKeys = Set() + for (rect, emoji) in emojiRects { + let index: Int + if let nextIndex = nextIndexById[emoji.fileId] { + index = nextIndex + } else { + index = 0 + } + nextIndexById[emoji.fileId] = index + 1 + + let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) + + let view: UIView + if let current = self.emojiLayers[key] { + view = current + } else if let newView = self.emojiViewProvider(emoji) { + view = newView + self.addSubview(newView) + self.emojiLayers[key] = view + } else { + continue + } + + if let view = view as? EmojiTextAttachmentView { + view.updateTextColor(textColor) + } + + let itemSize: CGFloat = floor(24.0 * fontSize / 17.0) + let size = CGSize(width: itemSize, height: itemSize) + + view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0) + 1.0), size: size) + + validKeys.insert(key) + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, view) in self.emojiLayers { + if !validKeys.contains(key) { + removeKeys.append(key) + view.removeFromSuperview() + } + } + for key in removeKeys { + self.emojiLayers.removeValue(forKey: key) + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 5298ee819c6..da95fdb49fb 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -40,6 +40,7 @@ swift_library( "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/LocalizedPeerData:LocalizedPeerData", + "//submodules/TelegramNotices:TelegramNotices", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 6f0f5596572..e2086001ce7 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -23,6 +23,7 @@ import AudioToolbox import SolidRoundedButtonComponent import EmojiTextAttachmentView import EmojiStatusComponent +import TelegramNotices private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) @@ -221,14 +222,16 @@ public final class EntityKeyboardAnimationData: Equatable { public let dimensions: CGSize public let immediateThumbnailData: Data? public let isReaction: Bool + public let isTemplate: Bool - public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool) { + public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool, isTemplate: Bool) { self.id = id self.type = type self.resource = resource self.dimensions = dimensions self.immediateThumbnailData = immediateThumbnailData self.isReaction = isReaction + self.isTemplate = isTemplate } public convenience init(file: TelegramMediaFile, isReaction: Bool = false) { @@ -240,7 +243,13 @@ public final class EntityKeyboardAnimationData: Equatable { } else { type = .still } - self.init(id: .file(file.fileId), type: type, resource: .standalone(resource: file.resource), dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction) + var isTemplate = false + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + isTemplate = isSingleColor + } + } + self.init(id: .file(file.fileId), type: type, resource: .standalone(resource: file.resource), dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction, isTemplate: isTemplate) } public static func ==(lhs: EntityKeyboardAnimationData, rhs: EntityKeyboardAnimationData) -> Bool { @@ -2092,9 +2101,9 @@ public final class EmojiPagerContentComponent: Component { public final class InputInteraction { public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void - public let deleteBackwards: () -> Void - public let openStickerSettings: () -> Void - public let openFeatured: () -> Void + public let deleteBackwards: (() -> Void)? + public let openStickerSettings: (() -> Void)? + public let openFeatured: (() -> Void)? public let openSearch: () -> Void public let addGroupAction: (AnyHashable, Bool) -> Void public let clearGroup: (AnyHashable) -> Void @@ -2104,18 +2113,20 @@ public final class EmojiPagerContentComponent: Component { public let navigationController: () -> NavigationController? public let requestUpdate: (Transition) -> Void public let updateSearchQuery: (String, String) -> Void + public let updateScrollingToItemGroup: () -> Void public let chatPeerId: PeerId? public let peekBehavior: EmojiContentPeekBehavior? public let customLayout: CustomLayout? public let externalBackground: ExternalBackground? public weak var externalExpansionView: UIView? public let useOpaqueTheme: Bool + public let hideBackground: Bool public init( performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void, - deleteBackwards: @escaping () -> Void, - openStickerSettings: @escaping () -> Void, - openFeatured: @escaping () -> Void, + deleteBackwards: (() -> Void)?, + openStickerSettings: (() -> Void)?, + openFeatured: (() -> Void)?, openSearch: @escaping () -> Void, addGroupAction: @escaping (AnyHashable, Bool) -> Void, clearGroup: @escaping (AnyHashable) -> Void, @@ -2125,12 +2136,14 @@ public final class EmojiPagerContentComponent: Component { navigationController: @escaping () -> NavigationController?, requestUpdate: @escaping (Transition) -> Void, updateSearchQuery: @escaping (String, String) -> Void, + updateScrollingToItemGroup: @escaping () -> Void, chatPeerId: PeerId?, peekBehavior: EmojiContentPeekBehavior?, customLayout: CustomLayout?, externalBackground: ExternalBackground?, externalExpansionView: UIView?, - useOpaqueTheme: Bool + useOpaqueTheme: Bool, + hideBackground: Bool ) { self.performItemAction = performItemAction self.deleteBackwards = deleteBackwards @@ -2145,12 +2158,14 @@ public final class EmojiPagerContentComponent: Component { self.navigationController = navigationController self.requestUpdate = requestUpdate self.updateSearchQuery = updateSearchQuery + self.updateScrollingToItemGroup = updateScrollingToItemGroup self.chatPeerId = chatPeerId self.peekBehavior = peekBehavior self.customLayout = customLayout self.externalBackground = externalBackground self.externalExpansionView = externalExpansionView self.useOpaqueTheme = useOpaqueTheme + self.hideBackground = hideBackground } } @@ -2200,12 +2215,18 @@ public final class EmojiPagerContentComponent: Component { case premium } + public enum TintMode { + case none + case accent + case primary + } + public let animationData: EntityKeyboardAnimationData? public let content: ItemContent public let itemFile: TelegramMediaFile? public let subgroupId: Int32? public let icon: Icon - public let accentTint: Bool + public let tintMode: TintMode public init( animationData: EntityKeyboardAnimationData?, @@ -2213,14 +2234,14 @@ public final class EmojiPagerContentComponent: Component { itemFile: TelegramMediaFile?, subgroupId: Int32?, icon: Icon, - accentTint: Bool + tintMode: TintMode ) { self.animationData = animationData self.content = content self.itemFile = itemFile self.subgroupId = subgroupId self.icon = icon - self.accentTint = accentTint + self.tintMode = tintMode } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -2242,7 +2263,7 @@ public final class EmojiPagerContentComponent: Component { if lhs.icon != rhs.icon { return false } - if lhs.accentTint != rhs.accentTint { + if lhs.tintMode != rhs.tintMode { return false } @@ -2893,14 +2914,14 @@ public final class EmojiPagerContentComponent: Component { return } - strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0)) + strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil)) } if attemptSynchronousLoad { if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) { self.updateDisplayPlaceholder(displayPlaceholder: true) - self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in + self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in if !isFinal { if !success { Queue.mainQueue().async { @@ -2930,7 +2951,7 @@ public final class EmojiPagerContentComponent: Component { loadAnimation() } } else { - self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in + self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in if !isFinal { if !success { Queue.mainQueue().async { @@ -5336,9 +5357,12 @@ public final class EmojiPagerContentComponent: Component { itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor) - if item.accentTint { + switch item.tintMode { + case .accent: itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor - } else { + case .primary: + itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemPrimaryTextColor.cgColor + case .none: itemLayer.layerTintColor = nil } @@ -5372,7 +5396,7 @@ public final class EmojiPagerContentComponent: Component { self.visibleItemSelectionLayers[itemId] = itemSelectionLayer } - if item.accentTint { + if case .accent = item.tintMode { itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor } else { @@ -5727,7 +5751,12 @@ public final class EmojiPagerContentComponent: Component { self.backgroundView.isHidden = false } - self.backgroundView.updateColor(color: keyboardChildEnvironment.theme.chat.inputMediaPanel.backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition) + let hideBackground = component.inputInteractionHolder.inputInteraction?.hideBackground ?? false + var backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.backgroundColor + if hideBackground { + backgroundColor = backgroundColor.withAlphaComponent(0.01) + } + self.backgroundView.updateColor(color: backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) @@ -6292,7 +6321,9 @@ public final class EmojiPagerContentComponent: Component { selectedItems: Set = Set(), topStatusTitle: String? = nil, topicTitle: String? = nil, - topicColor: Int32? = nil + topicColor: Int32? = nil, + hasSearch: Bool = true, + forceHasPremium: Bool = false ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -6344,7 +6375,7 @@ public final class EmojiPagerContentComponent: Component { let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), + forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), context.account.viewTracker.featuredEmojiPacks(), availableReactions, iconStatusEmoji @@ -6391,7 +6422,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) let groupId = "recent" @@ -6410,13 +6441,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6433,7 +6467,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6447,7 +6481,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) let groupId = "recent" @@ -6466,13 +6500,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6489,7 +6526,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6509,13 +6546,16 @@ public final class EmojiPagerContentComponent: Component { } existingIds.insert(file.fileId) - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6532,7 +6572,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6558,13 +6598,16 @@ public final class EmojiPagerContentComponent: Component { let resultItem: EmojiPagerContentComponent.Item - var accentTint = false + var tintMode: Item.TintMode = .none for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, isSingleColor, _, packReference) = attribute { + if isSingleColor { + tintMode = .accent + } switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { - accentTint = true + tintMode = .accent } default: break @@ -6579,7 +6622,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: accentTint + tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6636,6 +6679,15 @@ public final class EmojiPagerContentComponent: Component { icon = .none } + var tintMode: Item.TintMode = .none + for attribute in reactionItem.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationFile = reactionItem.file let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( @@ -6644,7 +6696,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let groupId = "recent" @@ -6691,6 +6743,15 @@ public final class EmojiPagerContentComponent: Component { icon = .none } + var tintMode: Item.TintMode = .none + for attribute in reactionItem.selectAnimation.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationFile = reactionItem.selectAnimation let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( @@ -6699,7 +6760,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) if hasPremium { @@ -6767,6 +6828,15 @@ public final class EmojiPagerContentComponent: Component { } } + var tintMode: Item.TintMode = .none + for attribute in animationFile.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6774,7 +6844,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: animationFile, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let groupId = "popular" @@ -6810,6 +6880,15 @@ public final class EmojiPagerContentComponent: Component { let resultItem: EmojiPagerContentComponent.Item switch item.content { case let .file(file): + var tintMode: Item.TintMode = .none + for attribute in file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6817,7 +6896,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) case let .text(text): resultItem = EmojiPagerContentComponent.Item( @@ -6826,7 +6905,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: .none ) } @@ -6856,6 +6935,19 @@ public final class EmojiPagerContentComponent: Component { icon = .locked } + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + if isStatusSelection { + tintMode = .accent + } else { + tintMode = .primary + } + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6863,7 +6955,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: icon, - accentTint: false + tintMode: tintMode ) let supergroupId = entry.index.collectionId @@ -6899,7 +6991,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } @@ -6917,6 +7010,19 @@ public final class EmojiPagerContentComponent: Component { } for item in featuredEmojiPack.topItems { + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + if isStatusSelection { + tintMode = .accent + } else { + tintMode = .primary + } + } + } + } + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -6924,7 +7030,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: item.file, subgroupId: nil, icon: .none, - accentTint: false + tintMode: tintMode ) let supergroupId = featuredEmojiPack.info.id @@ -6958,7 +7064,8 @@ public final class EmojiPagerContentComponent: Component { resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, - isReaction: false + isReaction: false, + isTemplate: false ) } @@ -6979,7 +7086,7 @@ public final class EmojiPagerContentComponent: Component { itemFile: nil, subgroupId: subgroupId.rawValue, icon: .none, - accentTint: false + tintMode: .none ) if let groupIndex = itemGroupIndexById[groupId] { @@ -6994,15 +7101,15 @@ public final class EmojiPagerContentComponent: Component { var displaySearchWithPlaceholder: String? var searchInitiallyHidden = true - if isReactionSelection { - displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder - } else if isStatusSelection { - displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder - } else if isTopicIconSelection { - displaySearchWithPlaceholder = strings.EmojiSearch_SearchTopicIconsPlaceholder - } else if isEmojiSelection { - displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder - searchInitiallyHidden = false + if hasSearch { + if isReactionSelection { + displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder + } else if isStatusSelection { + displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder + } else if isEmojiSelection { + displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder + searchInitiallyHidden = false + } } return EmojiPagerContentComponent( @@ -7059,6 +7166,541 @@ public final class EmojiPagerContentComponent: Component { } return emojiItems } + + public static func stickerInputData( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + stickerNamespaces: [ItemCollectionId.Namespace], + stickerOrderedItemListCollectionIds: [Int32], + chatPeerId: EnginePeer.Id?, + hasSearch: Bool, + hasTrending: Bool, + forceHasPremium: Bool + ) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + + struct PeerSpecificPackData: Equatable { + var info: StickerPackCollectionInfo + var items: [StickerPackItem] + var peer: EnginePeer + + static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { + if lhs.info.id != rhs.info.id { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.peer != rhs.peer { + return false + } + + return true + } + } + + let peerSpecificPack: Signal + if let chatPeerId = chatPeerId { + peerSpecificPack = combineLatest( + context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) + ) + |> map { packData, peer -> PeerSpecificPackData? in + guard let peer = peer else { + return nil + } + + guard let (info, items) = packData.packInfo else { + return nil + } + + return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) + } + |> distinctUntilChanged + } else { + peerSpecificPack = .single(nil) + } + + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings + + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), + forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), + hasTrending ? context.account.viewTracker.featuredStickerPacks() : .single([]), + context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), + ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), + peerSpecificPack + ) + |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String + var subtitle: String? + var actionButtonTitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var displayPremiumBadges: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + var savedStickers: OrderedItemListView? + var recentStickers: OrderedItemListView? + var cloudPremiumStickers: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { + recentStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { + savedStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudAllPremiumStickers { + cloudPremiumStickers = orderedView + } + } + + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + + let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? []) + let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id)) + + if dismissedTrendingStickerPacksSet != featuredStickerPacksSet { + let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) + for featuredStickerPack in featuredStickerPacks { + if installedCollectionIds.contains(featuredStickerPack.info.id) { + continue + } + + guard let item = featuredStickerPack.topItems.first else { + continue + } + + let animationData: EntityKeyboardAnimationData + + if let thumbnail = featuredStickerPack.info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + animationData = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(featuredStickerPack.info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, + isReaction: false, + isTemplate: false + ) + } else { + animationData = EntityKeyboardAnimationData(file: item.file) + } + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let supergroupId = "featuredTop" + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false + let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks + + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let savedStickers = savedStickers { + for item in savedStickers.items { + guard let item = item.contents.get(SavedStickerItem.self) else { + continue + } + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let groupId = "saved" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + if let recentStickers = recentStickers { + for item in recentStickers.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + + var tintMode: Item.TintMode = .none + for attribute in item.media.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.media) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.media, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + var premiumStickers: [StickerPackItem] = [] + if hasPremium { + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + + if item.file.isPremiumSticker { + premiumStickers.append(item) + } + } + + if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { + premiumStickers.append(contentsOf: cloudPremiumStickers.items.compactMap { item -> StickerPackItem? in guard let item = item.contents.get(RecentMediaItem.self) else { + return nil + } + return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.media, indexKeys: []) + }) + } + } + + if !premiumStickers.isEmpty { + var processedIds = Set() + for item in premiumStickers { + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + if processedIds.contains(item.file.fileId) { + continue + } + processedIds.insert(item.file.fileId) + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let groupId = "premium" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitlePremiumStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + var avatarPeer: EnginePeer? + if let peerSpecificPack = peerSpecificPack { + avatarPeer = peerSpecificPack.peer + + var processedIds = Set() + for item in peerSpecificPack.items { + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + if processedIds.contains(item.file.fileId) { + continue + } + processedIds.insert(item.file.fileId) + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let groupId = "peerSpecific" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + let groupId = entry.index.collectionId + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + var title = "" + var headerItem: EntityKeyboardAnimationData? + inner: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + title = info.title + + if let thumbnail = info.thumbnail { + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false, + isTemplate: false + ) + } + + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, headerItem: headerItem, items: [resultItem])) + } + } + + for featuredStickerPack in featuredStickerPacks { + if installedCollectionIds.contains(featuredStickerPack.info.id) { + continue + } + + for item in featuredStickerPack.topItems { + var tintMode: Item.TintMode = .none + for attribute in item.file.attributes { + if case let .CustomEmoji(_, isSingleColor, _, _) = attribute { + if isSingleColor { + tintMode = .primary + } + } + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + let supergroupId = featuredStickerPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + + let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count)) + var headerItem: EntityKeyboardAnimationData? + + if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { + headerItem = EntityKeyboardAnimationData(file: file.file) + } else if let thumbnail = featuredStickerPack.info.thumbnail { + let info = featuredStickerPack.info + let type: EntityKeyboardAnimationData.ItemType + if item.file.isAnimatedSticker { + type = .lottie + } else if item.file.isVideoEmoji || item.file.isVideoSticker { + type = .video + } else { + type = .still + } + + headerItem = EntityKeyboardAnimationData( + id: .stickerPackThumbnail(info.id), + type: type, + resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), + dimensions: thumbnail.dimensions.cgSize, + immediateThumbnailData: info.immediateThumbnailData, + isReaction: false, + isTemplate: false + ) + } + + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, headerItem: headerItem, items: [resultItem])) + } + } + } + + let isMasks = stickerNamespaces.contains(Namespaces.ItemCollection.CloudMaskPacks) + + return EmojiPagerContentComponent( + id: isMasks ? "masks" : "stickers", + context: context, + avatarPeer: avatarPeer, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var hasClear = false + var isEmbedded = false + if group.id == AnyHashable("recent") { + hasClear = true + } else if group.id == AnyHashable("featuredTop") { + hasClear = true + isEmbedded = true + } + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + actionButtonTitle: group.actionButtonTitle, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: isEmbedded, + hasClear: hasClear, + collapsedLineCount: nil, + displayPremiumBadges: group.displayPremiumBadges, + headerItem: group.headerItem, + items: group.items + ) + }, + itemLayoutType: .detailed, + itemContentUniqueId: nil, + warpContentsOnEdges: false, + displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, + searchInitiallyHidden: true, + searchIsPlaceholderOnly: true, + emptySearchResults: nil, + enableLongPress: false, + selectedItems: Set() + ) + } + } } func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], title: String) -> UIImage? { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 2c50f2fc07c..1e47db4f097 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -66,6 +66,7 @@ public final class EntityKeyboardComponent: Component { public enum ReorderCategory { case stickers case emoji + case masks } public struct GifSearchEmoji: Equatable { @@ -102,13 +103,16 @@ public final class EntityKeyboardComponent: Component { public let isContentInFocus: Bool public let containerInsets: UIEdgeInsets public let topPanelInsets: UIEdgeInsets - public let emojiContent: EmojiPagerContentComponent + public let emojiContent: EmojiPagerContentComponent? public let stickerContent: EmojiPagerContentComponent? + public let maskContent: EmojiPagerContentComponent? public let gifContent: GifPagerContentComponent? public let hasRecentGifs: Bool public let availableGifSearchEmojies: [GifSearchEmoji] public let defaultToEmojiTab: Bool public let externalTopPanelContainer: PagerExternalTopPanelContainer? + public let externalBottomPanelContainer: PagerExternalTopPanelContainer? + public let displayTopPanelBackground: Bool public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void public let hideInputUpdated: (Bool, Bool, Transition) -> Void public let hideTopPanelUpdated: (Bool, Transition) -> Void @@ -121,6 +125,7 @@ public final class EntityKeyboardComponent: Component { public let inputHeight: CGFloat public let displayBottomPanel: Bool public let isExpanded: Bool + public let clipContentToTopPanel: Bool public init( // MARK: Nicegram OpenGifsShortcut @@ -131,13 +136,16 @@ public final class EntityKeyboardComponent: Component { isContentInFocus: Bool, containerInsets: UIEdgeInsets, topPanelInsets: UIEdgeInsets, - emojiContent: EmojiPagerContentComponent, + emojiContent: EmojiPagerContentComponent?, stickerContent: EmojiPagerContentComponent?, + maskContent: EmojiPagerContentComponent?, gifContent: GifPagerContentComponent?, hasRecentGifs: Bool, availableGifSearchEmojies: [GifSearchEmoji], defaultToEmojiTab: Bool, externalTopPanelContainer: PagerExternalTopPanelContainer?, + externalBottomPanelContainer: PagerExternalTopPanelContainer?, + displayTopPanelBackground: Bool, topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void, hideTopPanelUpdated: @escaping (Bool, Transition) -> Void, @@ -149,7 +157,8 @@ public final class EntityKeyboardComponent: Component { hiddenInputHeight: CGFloat, inputHeight: CGFloat, displayBottomPanel: Bool, - isExpanded: Bool + isExpanded: Bool, + clipContentToTopPanel: Bool ) { // MARK: Nicegram OpenGifsShortcut self.defaultTab = defaultTab @@ -161,11 +170,14 @@ public final class EntityKeyboardComponent: Component { self.topPanelInsets = topPanelInsets self.emojiContent = emojiContent self.stickerContent = stickerContent + self.maskContent = maskContent self.gifContent = gifContent self.hasRecentGifs = hasRecentGifs self.availableGifSearchEmojies = availableGifSearchEmojies self.defaultToEmojiTab = defaultToEmojiTab self.externalTopPanelContainer = externalTopPanelContainer + self.externalBottomPanelContainer = externalBottomPanelContainer + self.displayTopPanelBackground = displayTopPanelBackground self.topPanelExtensionUpdated = topPanelExtensionUpdated self.hideInputUpdated = hideInputUpdated self.hideTopPanelUpdated = hideTopPanelUpdated @@ -178,6 +190,7 @@ public final class EntityKeyboardComponent: Component { self.inputHeight = inputHeight self.displayBottomPanel = displayBottomPanel self.isExpanded = isExpanded + self.clipContentToTopPanel = clipContentToTopPanel } public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { @@ -202,6 +215,9 @@ public final class EntityKeyboardComponent: Component { if lhs.stickerContent != rhs.stickerContent { return false } + if lhs.maskContent != rhs.maskContent { + return false + } if lhs.gifContent != rhs.gifContent { return false } @@ -217,6 +233,9 @@ public final class EntityKeyboardComponent: Component { if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { return false } + if lhs.displayTopPanelBackground != rhs.displayTopPanelBackground { + return false + } if lhs.deviceMetrics != rhs.deviceMetrics { return false } @@ -232,6 +251,9 @@ public final class EntityKeyboardComponent: Component { if lhs.isExpanded != rhs.isExpanded { return false } + if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel { + return false + } return true } @@ -286,11 +308,96 @@ public final class EntityKeyboardComponent: Component { let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() + let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() if transition.userData(MarkInputCollapsed.self) != nil { self.searchComponent = nil } + if let maskContent = component.maskContent { + var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = [] + + for itemGroup in maskContent.itemGroups { + if let id = itemGroup.supergroupId.base as? String { + let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ + "saved": .saved, + "recent": .recent, + "premium": .premium + ] + let titleMapping: [String: String] = [ + "saved": component.strings.Stickers_Favorites, + "recent": component.strings.Stickers_Recent, + "premium": component.strings.EmojiInput_PanelTitlePremium + ] + if let icon = iconMapping[id], let title = titleMapping[id] { + topMaskItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + icon: icon, + theme: component.theme, + useAccentColor: false, + title: title, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } else { + if !itemGroup.items.isEmpty { + if let animationData = itemGroup.items[0].animationData { + topMaskItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: !itemGroup.isFeatured, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: maskContent.context, + item: itemGroup.headerItem ?? animationData, + isFeatured: itemGroup.isFeatured, + isPremiumLocked: itemGroup.isPremiumLocked, + animationCache: maskContent.animationCache, + animationRenderer: maskContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } + } + } + contents.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(maskContent))) + contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "masks", + theme: component.theme, + items: topMaskItems, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, + defaultActiveItemId: maskContent.itemGroups.first?.groupId, + activeContentItemIdUpdated: masksContentItemIdUpdated, + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .masks, items: items) + } + )))) + contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon")) + if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + } + } + if let gifContent = component.gifContent { contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent))) var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] @@ -322,24 +429,26 @@ public final class EntityKeyboardComponent: Component { } )) )) - for emoji in component.availableGifSearchEmojies { - topGifItems.append(EntityKeyboardTopPanelComponent.Item( - id: emoji.emoji, - isReorderable: false, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.emojiContent.context, - item: EntityKeyboardAnimationData(file: emoji.file), - isFeatured: false, - isPremiumLocked: false, - animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer, - theme: component.theme, - title: emoji.title, - pressed: { [weak self] in - self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) - } + if let emojiContent = component.emojiContent { + for emoji in component.availableGifSearchEmojies { + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: emoji.emoji, + isReorderable: false, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: emojiContent.context, + item: EntityKeyboardAnimationData(file: emoji.file), + isFeatured: false, + isPremiumLocked: false, + animationCache: emojiContent.animationCache, + animationRenderer: emojiContent.animationRenderer, + theme: component.theme, + title: emoji.title, + pressed: { [weak self] in + self?.component?.switchToGifSubject(.emojiSearch(emoji.emoji)) + } + )) )) - )) + } } let defaultActiveGifItemId: AnyHashable switch gifContent.subject { @@ -361,34 +470,26 @@ public final class EntityKeyboardComponent: Component { } )))) contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon")) -// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( -// content: AnyComponent(BundleIconComponent( -// name: "Chat/Input/Media/EntityInputSearchIcon", -// tintColor: component.theme.chat.inputMediaPanel.panelIconColor, -// maxSize: nil -// )), -// action: { [weak self] in -// self?.openSearch() -// } -// ).minSize(CGSize(width: 38.0, height: 38.0))))) } if let stickerContent = component.stickerContent { var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] - topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: "featuredTop", - isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - icon: .featured, - theme: component.theme, - useAccentColor: false, - title: component.strings.Stickers_Trending, - pressed: { [weak self] in - self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured() - } + if let _ = stickerContent.inputInteractionHolder.inputInteraction?.openFeatured { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: "featuredTop", + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + icon: .featured, + theme: component.theme, + useAccentColor: false, + title: component.strings.Stickers_Trending, + pressed: { [weak self] in + self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured?() + } + )) )) - )) + } for itemGroup in stickerContent.itemGroups { if let id = itemGroup.supergroupId.base as? String { @@ -475,138 +576,138 @@ public final class EntityKeyboardComponent: Component { } )))) contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon")) -// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( -// content: AnyComponent(BundleIconComponent( -// name: "Chat/Input/Media/EntityInputSearchIcon", -// tintColor: component.theme.chat.inputMediaPanel.panelIconColor, -// maxSize: nil -// )), -// action: { [weak self] in -// self?.openSearch() -// } -// ).minSize(CGSize(width: 38.0, height: 38.0))))) - contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputSettingsIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { - stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) + if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings { + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + } } + let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards + let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() - contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) - var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] - for itemGroup in component.emojiContent.itemGroups { - if !itemGroup.items.isEmpty { - if let id = itemGroup.groupId.base as? String { - if id == "recent" { - let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ - "recent": .recent, - ] - let titleMapping: [String: String] = [ - "recent": component.strings.Stickers_Recent, - ] - if let icon = iconMapping[id], let title = titleMapping[id] { + if let emojiContent = component.emojiContent { + contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) + var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in emojiContent.itemGroups { + if !itemGroup.items.isEmpty { + if let id = itemGroup.groupId.base as? String { + if id == "recent" { + let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ + "recent": .recent, + ] + let titleMapping: [String: String] = [ + "recent": component.strings.Stickers_Recent, + ] + if let icon = iconMapping[id], let title = titleMapping[id] { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + icon: icon, + theme: component.theme, + useAccentColor: false, + title: title, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } else if id == "static" { topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - icon: icon, + content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( theme: component.theme, - useAccentColor: false, - title: title, - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) + title: component.strings.EmojiInput_PanelTitleEmoji, + pressed: { [weak self] subgroupId in + guard let strongSelf = self else { + return + } + strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) } )) )) } - } else if id == "static" { - topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: false, - content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( - theme: component.theme, - title: component.strings.EmojiInput_PanelTitleEmoji, - pressed: { [weak self] subgroupId in - guard let strongSelf = self else { - return + } else { + if let animationData = itemGroup.items[0].animationData { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: !itemGroup.isFeatured, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: emojiContent.context, + item: itemGroup.headerItem ?? animationData, + isFeatured: itemGroup.isFeatured, + isPremiumLocked: itemGroup.isPremiumLocked, + animationCache: emojiContent.animationCache, + animationRenderer: emojiContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) } - strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) - } - )) - )) - } - } else { - if let animationData = itemGroup.items[0].animationData { - topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: !itemGroup.isFeatured, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.emojiContent.context, - item: itemGroup.headerItem ?? animationData, - isFeatured: itemGroup.isFeatured, - isPremiumLocked: itemGroup.isPremiumLocked, - animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer, - theme: component.theme, - title: itemGroup.title ?? "", - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) - } + )) )) - )) + } } } } - } - contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( - id: "emoji", - theme: component.theme, - items: topEmojiItems, - containerSideInset: component.containerInsets.left + component.topPanelInsets.left, - activeContentItemIdUpdated: emojiContentItemIdUpdated, - activeContentItemMapping: ["popular": "recent"], - reorderItems: { [weak self] items in - guard let strongSelf = self else { - return + contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( + id: "emoji", + theme: component.theme, + items: topEmojiItems, + containerSideInset: component.containerInsets.left + component.topPanelInsets.left, + activeContentItemIdUpdated: emojiContentItemIdUpdated, + activeContentItemMapping: ["popular": "recent"], + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .emoji, items: items) } - strongSelf.reorderPacks(category: .emoji, items: items) + )))) + contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon")) + if let _ = deleteBackwards { + contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGlobeIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { [weak self] in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + component.switchToTextInput() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) } - )))) - contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon")) - contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputGlobeIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { [weak self] in - guard let strongSelf = self, let component = strongSelf.component else { - return + } + + if let _ = deleteBackwards { + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputClearIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + deleteBackwards?() + AudioServicesPlaySystemSound(1155) } - component.switchToTextInput() - } - ).minSize(CGSize(width: 38.0, height: 38.0))))) - let deleteBackwards = component.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards - contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputClearIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { + ).withHoldAction({ deleteBackwards?() AudioServicesPlaySystemSound(1155) - } - ).withHoldAction({ - deleteBackwards?() - AudioServicesPlaySystemSound(1155) - }).minSize(CGSize(width: 38.0, height: 38.0))))) + }).minSize(CGSize(width: 38.0, height: 38.0))))) + } let panelHideBehavior: PagerComponentPanelHideBehavior if self.searchComponent != nil { @@ -633,17 +734,18 @@ public final class EntityKeyboardComponent: Component { topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( theme: component.theme, overflowHeight: component.hiddenInputHeight, - displayBackground: component.externalTopPanelContainer == nil + displayBackground: component.externalTopPanelContainer == nil && component.displayTopPanelBackground )), externalTopPanelContainer: component.externalTopPanelContainer, bottomPanel: component.displayBottomPanel ? AnyComponent(EntityKeyboardBottomPanelComponent( theme: component.theme, containerInsets: component.containerInsets, deleteBackwards: { [weak self] in - self?.component?.emojiContent.inputInteractionHolder.inputInteraction?.deleteBackwards() + self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards?() AudioServicesPlaySystemSound(0x451) } )) : nil, + externalBottomPanelContainer: component.externalBottomPanelContainer, panelStateUpdated: { [weak self] panelState, transition in guard let strongSelf = self else { return @@ -662,7 +764,8 @@ public final class EntityKeyboardComponent: Component { } strongSelf.isTopPanelHiddenUpdated(isTopPanelHidden: isTopPanelHidden, transition: transition) }, - panelHideBehavior: panelHideBehavior + panelHideBehavior: panelHideBehavior, + clipContentToTopPanel: component.clipContentToTopPanel )), environment: { EntityKeyboardChildEnvironment( @@ -676,6 +779,8 @@ public final class EntityKeyboardComponent: Component { return stickersContentItemIdUpdated } else if id == AnyHashable("emoji") { return emojiContentItemIdUpdated + } else if id == AnyHashable("masks") { + return masksContentItemIdUpdated } return nil } @@ -685,7 +790,8 @@ public final class EntityKeyboardComponent: Component { ) transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) - if let searchComponent = self.searchComponent { + let accountContext = component.emojiContent?.context ?? component.stickerContent?.context + if let searchComponent = self.searchComponent, let accountContext = accountContext { var animateIn = false let searchView: ComponentHostView var searchViewTransition = transition @@ -706,7 +812,7 @@ public final class EntityKeyboardComponent: Component { component: AnyComponent(searchComponent), environment: { EntitySearchContentEnvironment( - context: component.emojiContent.context, + context: accountContext, theme: component.theme, deviceMetrics: component.deviceMetrics, inputHeight: component.inputHeight @@ -827,6 +933,8 @@ public final class EntityKeyboardComponent: Component { if let topPanelView = pagerView.topPanelComponentView as? EntityKeyboardTopContainerPanelComponent.View { topPanelView.internalUpdatePanelsAreCollapsed() } + self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.updateScrollingToItemGroup() + pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) pagerView.collapseTopPanel() } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 2a961097686..4f5cdf4ca01 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -116,7 +116,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { itemFile: nil, subgroupId: nil, icon: .none, - accentTint: false + tintMode: component.item.isTemplate ? .primary : .none ), context: component.context, attemptSynchronousLoad: false, @@ -158,6 +158,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } itemLayer.update(transition: transition, size: iconFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: component.theme.list.plainBackgroundColor) + switch itemLayer.item.tintMode { + case .none: + break + case .primary: + itemLayer.layerTintColor = component.theme.list.itemPrimaryTextColor.cgColor + case .accent: + itemLayer.layerTintColor = component.theme.list.itemAccentColor.cgColor + } + itemLayer.isVisibleForAnimations = true } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 9bf011d9eb7..84da5e95050 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -23,6 +23,7 @@ import ShimmerEffect private class GifVideoLayer: AVSampleBufferDisplayLayer { private let context: AccountContext + private let userLocation: MediaResourceUserLocation private let file: TelegramMediaFile? private var frameManager: SoftwareVideoLayerFrameManager? @@ -59,8 +60,9 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { } } - init(context: AccountContext, file: TelegramMediaFile?, synchronousLoad: Bool) { + init(context: AccountContext, userLocation: MediaResourceUserLocation, file: TelegramMediaFile?, synchronousLoad: Bool) { self.context = context + self.userLocation = userLocation self.file = file super.init() @@ -69,7 +71,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { if let file = self.file { if let dimensions = file.dimensions { - self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) + self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, userLocation: userLocation, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) |> deliverOnMainQueue).start(next: { [weak self] transform in guard let strongSelf = self else { return @@ -111,7 +113,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { guard let file = self.file else { return } - let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: file), layerHolder: nil, layer: self) + let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .other, fileReference: .savedGif(media: file), layerHolder: nil, layer: self) self.frameManager = frameManager frameManager.started = { [weak self] in guard let strongSelf = self else { @@ -352,7 +354,7 @@ public final class GifPagerContentComponent: Component { self.item = item self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder - super.init(context: context, file: item?.file.media, synchronousLoad: attemptSynchronousLoad) + super.init(context: context, userLocation: .other, file: item?.file.media, synchronousLoad: attemptSynchronousLoad) if item == nil { self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0) diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 3a5dc7880c2..5cf399cfda2 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -401,11 +401,14 @@ private final class TopicIconSelectionComponent: Component { topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), emojiContent: component.emojiContent, stickerContent: nil, + maskContent: nil, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: true, externalTopPanelContainer: self.panelHostView, + externalBottomPanelContainer: nil, + displayTopPanelBackground: true, topPanelExtensionUpdated: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { _, _ in }, @@ -417,7 +420,8 @@ private final class TopicIconSelectionComponent: Component { hiddenInputHeight: 0.0, inputHeight: 0.0, displayBottomPanel: false, - isExpanded: true + isExpanded: true, + clipContentToTopPanel: false )), environment: {}, containerSize: availableSize @@ -960,12 +964,15 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { }, updateSearchQuery: { _, _ in }, + updateScrollingToItemGroup: { + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, - useOpaqueTheme: true + useOpaqueTheme: true, + hideBackground: false ) } } diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 96b56e41c03..21265ad2fab 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -6,7 +6,7 @@ import RLottieBinding import GZip import WebPBinding -public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOnly: Bool, writer: AnimationCacheItemWriter, firstFrameOnly: Bool) { +public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOnly: Bool, writer: AnimationCacheItemWriter, firstFrameOnly: Bool, customColor: UIColor?) { let work: () -> Void = { let decompressedData = TGGUnzipData(data, 2 * 1024 * 1024) ?? data guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { @@ -32,6 +32,19 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOn } writer.add(with: { surface in animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow)) + if customColor != nil { + for y in 0 ..< surface.height { + for x in 0 ..< surface.width { + let pixel = surface.argb.advanced(by: y * surface.bytesPerRow + x * 4) + let a = pixel.advanced(by: 3).pointee + + pixel.advanced(by: 0).pointee = a + pixel.advanced(by: 1).pointee = a + pixel.advanced(by: 2).pointee = a + pixel.advanced(by: 3).pointee = a + } + } + } return frameDuration }, proposedWidth: width, proposedHeight: height, insertKeyframe: i == 0 || keyframeOnly) @@ -46,7 +59,7 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, keyframeOn writer.queue.async(work) } -public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { +public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, customColor: UIColor?) { let work: () -> Void = { if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) { writer.add(with: { surface in @@ -55,6 +68,12 @@ public func cacheStillSticker(path: String, width: Int, height: Int, writer: Ani } context.withFlippedContext { c in UIGraphicsPushContext(c) + + if let customColor = customColor { + c.setFillColor(customColor.cgColor) + c.setBlendMode(.sourceIn) + } + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size)) UIGraphicsPopContext() } diff --git a/submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD b/submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD new file mode 100644 index 00000000000..dbbd2364a66 --- /dev/null +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MultiplexedVideoNode", + module_name = "MultiplexedVideoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/ContextUI:ContextUI", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/SoftwareVideo:SoftwareVideo", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift similarity index 93% rename from submodules/TelegramUI/Sources/MultiplexedVideoNode.swift rename to submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift index 80ba7707d08..aec600797c6 100644 --- a/submodules/TelegramUI/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift @@ -74,24 +74,24 @@ private final class VisibleVideoItem { } } -final class MultiplexedVideoNodeFile { - let file: FileMediaReference - let contextResult: (ChatContextResultCollection, ChatContextResult)? +public final class MultiplexedVideoNodeFile { + public let file: FileMediaReference + public let contextResult: (ChatContextResultCollection, ChatContextResult)? - init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) { + public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) { self.file = file self.contextResult = contextResult } } -final class MultiplexedVideoNodeFiles { - let saved: [MultiplexedVideoNodeFile] - let trending: [MultiplexedVideoNodeFile] - let isSearch: Bool - let canLoadMore: Bool - let isStale: Bool - - init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) { +public final class MultiplexedVideoNodeFiles { + public let saved: [MultiplexedVideoNodeFile] + public let trending: [MultiplexedVideoNodeFile] + public let isSearch: Bool + public let canLoadMore: Bool + public let isStale: Bool + + public init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) { self.saved = saved self.trending = trending self.isSearch = isSearch @@ -100,36 +100,37 @@ final class MultiplexedVideoNodeFiles { } } -final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { +public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private let account: Account private var theme: PresentationTheme private var strings: PresentationStrings private let trackingNode: MultiplexedVideoTrackingNode - var didScroll: ((CGFloat, CGFloat) -> Void)? - var didEndScrolling: (() -> Void)? - var reactionSelected: ((String) -> Void)? - var topInset: CGFloat = 0.0 { + public var didScroll: ((CGFloat, CGFloat) -> Void)? + public var didEndScrolling: (() -> Void)? + public var reactionSelected: ((String) -> Void)? + + public var topInset: CGFloat = 0.0 { didSet { self.setNeedsLayout() } } - var bottomInset: CGFloat = 0.0 { + public var bottomInset: CGFloat = 0.0 { didSet { self.setNeedsLayout() } } - var idealHeight: CGFloat = 93.0 { + public var idealHeight: CGFloat = 93.0 { didSet { self.setNeedsLayout() } } - private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false) + public private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false) - func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) { + public func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) { self.files = files self.ignoreDidScroll = true @@ -145,7 +146,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:] private let contextContainerNode: ContextControllerSourceNode - let scrollNode: ASScrollNode + public let scrollNode: ASScrollNode private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:] @@ -157,14 +158,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private let timebase: CMTimebase - var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)? - var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? - var enableVideoNodes = false + public var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)? + public var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? + public var enableVideoNodes = false private var currentActivatingId: VisibleVideoItem.Id? private var isFinishingActivation = false - init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { + public init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account self.theme = theme self.strings = strings @@ -343,7 +344,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } private var validSize: CGSize? - func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) { self.theme = theme self.strings = strings if self.validSize == nil || !self.validSize!.equalTo(size) { @@ -359,18 +360,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { private var ignoreDidScroll: Bool = false - func scrollViewDidScroll(_ scrollView: UIScrollView) { + public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreDidScroll { self.updateImmediatelyVisibleItems() self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height) } } - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.didEndScrolling?() } - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.didEndScrolling?() } @@ -463,7 +464,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.frame = item.frame self.scrollNode.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.file.file, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: item.file.file, layerHolder: layerHolder) self.visibleLayers[item.id] = (manager, layerHolder) self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in if let strongSelf = self { @@ -738,7 +739,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { } } - func frameForItem(_ id: MediaId) -> CGRect? { + public func frameForItem(_ id: MediaId) -> CGRect? { for item in self.displayItems { if item.file.file.media.fileId == id { return item.frame @@ -747,7 +748,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { return nil } - func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? { + public func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? { if let result = self.internalFileAt(point: point) { return (result.1, result.2, result.3) } else { diff --git a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift similarity index 87% rename from submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift rename to submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift index e54058df92e..bf26679418d 100644 --- a/submodules/TelegramUI/Sources/SoftwareVideoThumbnailLayer.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift @@ -13,7 +13,7 @@ private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction { } } -final class SoftwareVideoThumbnailNode: ASDisplayNode { +public final class SoftwareVideoThumbnailNode: ASDisplayNode { private let usePlaceholder: Bool private var placeholder: MultiplexedVideoPlaceholderNode? private var theme: PresentationTheme? @@ -21,7 +21,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode { var disposable = MetaDisposable() - var ready: (() -> Void)? { + public var ready: (() -> Void)? { didSet { if self.layer.contents != nil { self.ready?() @@ -29,6 +29,10 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode { } } + public convenience init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false) { + self.init(account: account, fileReference: fileReference, synchronousLoad: synchronousLoad, existingPlaceholder: nil) + } + init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false, existingPlaceholder: MultiplexedVideoPlaceholderNode? = nil) { self.usePlaceholder = usePlaceholder if usePlaceholder { @@ -52,7 +56,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode { self.layer.masksToBounds = true if let dimensions = fileReference.media.dimensions { - self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad, nilForEmptyResult: true) + self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, userLocation: .other, videoReference: fileReference, synchronousLoad: synchronousLoad, nilForEmptyResult: true) |> deliverOnMainQueue).start(next: { [weak self] transform in var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0)) let imageSize = boundingSize @@ -102,7 +106,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode { self.disposable.dispose() } - func update(theme: PresentationTheme, size: CGSize) { + public func update(theme: PresentationTheme, size: CGSize) { if self.usePlaceholder { self.theme = theme } @@ -112,7 +116,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode { } } - func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { self.asolutePosition = (absoluteRect, containerSize) if let placeholder = self.placeholder { placeholder.updateAbsoluteRect(absoluteRect, within: containerSize) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD new file mode 100644 index 00000000000..1d94ad70e4d --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "StorageUsageScreen", + module_name = "StorageUsageScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/CheckNode", + "//submodules/Markdown", + "//submodules/ContextUI", + "//submodules/AnimatedAvatarSetNode", + "//submodules/AvatarNode", + "//submodules/PhotoResources", + "//submodules/SemanticStatusNode", + "//submodules/RadialStatusNode", + "//submodules/UndoUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/LegacyComponents", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift new file mode 100644 index 00000000000..27d83961452 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -0,0 +1,582 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox + +private func interpolateChartData(start: PieChartComponent.ChartData, end: PieChartComponent.ChartData, progress: CGFloat) -> PieChartComponent.ChartData { + if start.items.count != end.items.count { + return start + } + + var result = end + for i in 0 ..< result.items.count { + result.items[i].value = (1.0 - progress) * start.items[i].value + progress * end.items[i].value + result.items[i].color = start.items[i].color.interpolateTo(end.items[i].color, fraction: progress) ?? end.items[i].color + } + + return result +} + +private func processChartData(data: PieChartComponent.ChartData) -> PieChartComponent.ChartData { + var data = data + + let minValue: Double = 0.01 + + var totalSum: CGFloat = 0.0 + for i in 0 ..< data.items.count { + if data.items[i].value > 0.00001 { + data.items[i].value = max(data.items[i].value, minValue) + } + totalSum += data.items[i].value + } + + var hasOneItem = false + for i in 0 ..< data.items.count { + if data.items[i].value != 0 && totalSum == data.items[i].value { + data.items[i].value = 1.0 + hasOneItem = true + break + } + } + + if !hasOneItem { + if abs(totalSum - 1.0) > 0.0001 { + let deltaValue = totalSum - 1.0 + + var availableSum: Double = 0.0 + for i in 0 ..< data.items.count { + let itemValue = data.items[i].value + let availableItemValue = max(0.0, itemValue - minValue) + if availableItemValue > 0.0 { + availableSum += availableItemValue + } + } + totalSum = 0.0 + let itemFraction = deltaValue / availableSum + for i in 0 ..< data.items.count { + let itemValue = data.items[i].value + let availableItemValue = max(0.0, itemValue - minValue) + if availableItemValue > 0.0 { + let itemDelta = availableItemValue * itemFraction + data.items[i].value -= itemDelta + } + totalSum += data.items[i].value + } + } + + if totalSum > 0.0 && totalSum < 1.0 - 0.0001 { + for i in 0 ..< data.items.count { + data.items[i].value /= totalSum + } + } + } + + return data +} + +private let chartLabelFont = Font.with(size: 16.0, design: .round, weight: .semibold) + +private final class ChartLabel: UIView { + private let label: ImmediateTextView + private var currentText: String? + + override init(frame: CGRect) { + self.label = ImmediateTextView() + + super.init(frame: frame) + + self.addSubview(self.label) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(text: String) -> CGSize { + if self.currentText == text { + return self.label.bounds.size + } + + var snapshotView: UIView? + if self.currentText != nil { + snapshotView = self.label.snapshotView(afterScreenUpdates: false) + snapshotView?.frame = self.label.frame + } + + self.currentText = text + self.label.attributedText = NSAttributedString(string: text, font: chartLabelFont, textColor: .white) + let size = self.label.updateLayout(CGSize(width: 100.0, height: 100.0)) + self.label.frame = CGRect(origin: CGPoint(x: floor(-size.width * 0.5), y: floor(-size.height * 0.5)), size: size) + + if let snapshotView { + self.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + self.label.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.label.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) + } + + return size + } +} + +final class PieChartComponent: Component { + struct ChartData: Equatable { + struct Item: Equatable { + var id: StorageUsageScreenComponent.Category + var displayValue: Double + var value: Double + var color: UIColor + + init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor) { + self.id = id + self.displayValue = displayValue + self.value = value + self.color = color + } + } + + var items: [Item] + + init(items: [Item]) { + self.items = items + } + } + + let theme: PresentationTheme + let chartData: ChartData + + init( + theme: PresentationTheme, + chartData: ChartData + ) { + self.theme = theme + self.chartData = chartData + } + + static func ==(lhs: PieChartComponent, rhs: PieChartComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.chartData != rhs.chartData { + return false + } + return true + } + + private final class ChartDataView: UIView { + private(set) var theme: PresentationTheme? + private(set) var data: ChartData? + private(set) var selectedKey: StorageUsageScreenComponent.Category? + + private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? + private var animator: DisplayLinkAnimator? + + private var labels: [StorageUsageScreenComponent.Category: ChartLabel] = [:] + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = nil + self.isOpaque = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.animator?.invalidate() + } + + func setItems(theme: PresentationTheme, data: ChartData, selectedKey: StorageUsageScreenComponent.Category?, animated: Bool) { + let data = processChartData(data: data) + + if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { + self.theme = theme + self.selectedKey = selectedKey + + if animated, let previous = self.data { + var initialState = previous + if let currentAnimation = self.currentAnimation { + initialState = currentAnimation.current + } + self.currentAnimation = (initialState, data, initialState, 0.0) + self.animator?.invalidate() + self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in + guard let self else { + return + } + let progress = listViewAnimationCurveSystem(progress) + if let currentAnimationValue = self.currentAnimation { + self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress) + self.setNeedsDisplay() + } + }, completion: { [weak self] in + guard let self else { + return + } + self.currentAnimation = nil + self.setNeedsDisplay() + }) + } + + self.data = data + + self.setNeedsDisplay() + } + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + guard let _ = self.theme, let data = self.currentAnimation?.current ?? self.data else { + return + } + if data.items.isEmpty { + return + } + + let innerDiameter: CGFloat = 100.0 + let spacing: CGFloat = 2.0 + let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) + //let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5) + + var angles: [Double] = [] + for i in 0 ..< data.items.count { + let item = data.items[i] + let angle = item.value * CGFloat.pi * 2.0 + angles.append(angle) + } + + let diameter: CGFloat = 200.0 + let reducedDiameter: CGFloat = 170.0 + + var startAngle: CGFloat = 0.0 + for i in 0 ..< data.items.count { + let item = data.items[i] + + let itemOuterDiameter: CGFloat + if let selectedKey = self.selectedKey { + if selectedKey == item.id { + itemOuterDiameter = diameter + } else { + itemOuterDiameter = reducedDiameter + } + } else { + itemOuterDiameter = diameter + } + + let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)) + + let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5) + + let angleValue: CGFloat = angles[i] + + let innerStartAngle = startAngle + innerAngleSpacing * 0.5 + var innerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 + innerEndAngle = max(innerEndAngle, innerStartAngle) + + let outerStartAngle = startAngle + angleSpacing * 0.5 + var outerEndAngle = startAngle + angleValue - angleSpacing * 0.5 + outerEndAngle = max(outerEndAngle, outerStartAngle) + + let path = CGMutablePath() + + path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: innerEndAngle, endAngle: innerStartAngle, clockwise: true) + path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false) + + context.addPath(path) + context.setFillColor(item.color.cgColor) + context.fillPath() + + startAngle += angleValue + + let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0 + let fractionString: String + if fractionValue < 0.1 { + fractionString = "<0.1" + } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { + fractionString = "\(Int(fractionValue))" + } else { + fractionString = "\(fractionValue)" + } + + let label: ChartLabel + if let current = self.labels[item.id] { + label = current + } else { + label = ChartLabel() + self.labels[item.id] = label + } + let labelSize = label.update(text: "\(fractionString)%") + + var labelFrame: CGRect? + + if angleValue >= 0.001 { + for step in 0 ... 20 { + let stepFraction: CGFloat = CGFloat(step) / 20.0 + let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction + + let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 + let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset) + + let relLabelCenter = CGPoint( + x: cos(midAngle) * centerDistance, + y: sin(midAngle) * centerDistance + ) + + let labelCenter = CGPoint( + x: shapeLayerFrame.midX + relLabelCenter.x, + y: shapeLayerFrame.midY + relLabelCenter.y + ) + + func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat { + let dx: CGFloat = p2.x - p1.x + let dy: CGFloat = p2.y - p1.y + let dr: CGFloat = sqrt(dx * dx + dy * dy) + let D: CGFloat = p1.x * p2.y - p2.x * p1.y + + var minDistance: CGFloat = 10000.0 + + for i in 0 ..< 2 { + let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0) + let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0 + let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0)) + minDistance = min(minDistance, distance) + } + + return minDistance + } + + func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat { + let x1 = p1.x + let y1 = p1.y + let x2 = p2.x + let y2 = p2.y + let x3 = p3.x + let y3 = p3.y + let x4 = p4.x + let y4 = p4.y + + let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if abs(d) <= 0.00001 { + return 10000.0 + } + + let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d + let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d + + let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0)) + return distance + } + + let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5) + let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5) + let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5) + let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5) + + let horizontalInset: CGFloat = 2.0 + let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), diameter * 0.5) - horizontalInset + let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerDiameter * 0.5) - horizontalInset + + let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) + let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) + let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) + let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) + + var distances: [CGFloat] = [ + intersectionOuterTopRight, + intersectionInnerTopRight, + intersectionOuterBottomRight, + intersectionInnerBottomRight, + intersectionOuterLeft, + intersectionInnerLeft + ] + + if angleValue < CGFloat.pi / 2.0 { + distances.append(contentsOf: [ + intersectionLine1TopRight, + intersectionLine1BottomRight, + intersectionLine2TopRight, + intersectionLine2BottomRight + ] as [CGFloat]) + } + + var minDistance: CGFloat = 1000.0 + for distance in distances { + minDistance = min(minDistance, max(distance, 1.0)) + } + + let diagonalAngle = atan2(labelSize.height, labelSize.width) + + let maxHalfWidth = cos(diagonalAngle) * minDistance + let maxHalfHeight = sin(diagonalAngle) * minDistance + + let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0) + let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height)) + + let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize) + + if finalSize.width >= labelSize.width { + labelFrame = currentFrame + break + } + if let labelFrame { + if labelFrame.width > finalSize.width { + continue + } + } + labelFrame = currentFrame + } + } else { + let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 + let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5) + + let relLabelCenter = CGPoint( + x: cos(midAngle) * centerDistance, + y: sin(midAngle) * centerDistance + ) + + let labelCenter = CGPoint( + x: shapeLayerFrame.midX + relLabelCenter.x, + y: shapeLayerFrame.midY + relLabelCenter.y + ) + + let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0)) + labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize) + } + + let labelView = label + if let labelFrame { + var animateIn: Bool = false + if labelView.superview == nil { + animateIn = true + self.addSubview(labelView) + } + + var labelScale = labelFrame.width / labelSize.width + + let normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0 + + var relLabelCenter = CGPoint( + x: labelFrame.midX - shapeLayerFrame.midX, + y: labelFrame.midY - shapeLayerFrame.midY + ) + + let labelAlpha: CGFloat + if let selectedKey = self.selectedKey { + if selectedKey == item.id { + labelAlpha = normalAlpha + } else { + labelAlpha = 0.0 + + let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter) + let reducedDiameterFactor: CGFloat = reducedDiameter / diameter + + labelScale *= reducedFactor + + relLabelCenter.x *= reducedDiameterFactor + relLabelCenter.y *= reducedDiameterFactor + } + } else { + labelAlpha = normalAlpha + } + if labelView.alpha != labelAlpha { + let transition: Transition + if animateIn { + transition = .immediate + } else { + transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + } + transition.setAlpha(view: labelView, alpha: labelAlpha) + } + + let labelCenter = CGPoint( + x: shapeLayerFrame.midX + relLabelCenter.x, + y: shapeLayerFrame.midY + relLabelCenter.y + ) + + labelView.center = labelCenter + labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale) + } + } + } + } + + class View: UIView { + private let dataView: ChartDataView + private var labels: [StorageUsageScreenComponent.Category: ComponentView] = [:] + var selectedKey: StorageUsageScreenComponent.Category? + + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.dataView = ChartDataView() + + super.init(frame: frame) + + self.addSubview(self.dataView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self) + let _ = point + /*for (key, layer) in self.shapeLayers { + if layer.frame.contains(point), let path = layer.path { + if path.contains(self.layer.convert(point, to: layer)) { + if self.selectedKey == key { + self.selectedKey = nil + } else { + self.selectedKey = key + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + + break + } + } + }*/ + } + } + + func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + + transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0))) + self.dataView.setItems(theme: component.theme, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate) + + return CGSize(width: availableSize.width, height: 200.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift new file mode 100644 index 00000000000..dd22e303781 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift @@ -0,0 +1,265 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import CheckNode +import SolidRoundedButtonComponent + +final class StorageCategoriesComponent: Component { + struct CategoryData: Equatable { + var key: StorageUsageScreenComponent.Category + var color: UIColor + var title: String + var size: Int64 + var sizeFraction: Double + var isSelected: Bool + var subcategories: [CategoryData] + + init(key: StorageUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) { + self.key = key + self.title = title + self.color = color + self.size = size + self.sizeFraction = sizeFraction + self.isSelected = isSelected + self.subcategories = subcategories + } + } + + let theme: PresentationTheme + let strings: PresentationStrings + let categories: [CategoryData] + let isOtherExpanded: Bool + let toggleCategorySelection: (StorageUsageScreenComponent.Category) -> Void + let toggleOtherExpanded: () -> Void + let clearAction: () -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + categories: [CategoryData], + isOtherExpanded: Bool, + toggleCategorySelection: @escaping (StorageUsageScreenComponent.Category) -> Void, + toggleOtherExpanded: @escaping () -> Void, + clearAction: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.categories = categories + self.isOtherExpanded = isOtherExpanded + self.toggleCategorySelection = toggleCategorySelection + self.toggleOtherExpanded = toggleOtherExpanded + self.clearAction = clearAction + } + + static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.categories != rhs.categories { + return false + } + if lhs.isOtherExpanded != rhs.isOtherExpanded { + return false + } + return true + } + + class View: UIView { + private var itemViews: [StorageUsageScreenComponent.Category: ComponentView] = [:] + private let button = ComponentView() + + private var component: StorageCategoriesComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = true + self.layer.cornerRadius = 10.0 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let expandedCategory: StorageUsageScreenComponent.Category? = component.isOtherExpanded ? .other : nil + + var totalSelectedSize: Int64 = 0 + var hasDeselected = false + for category in component.categories { + if !category.subcategories.isEmpty { + for subcategory in category.subcategories { + if subcategory.isSelected { + totalSelectedSize += subcategory.size + } else { + hasDeselected = true + } + } + } else { + if category.isSelected { + totalSelectedSize += category.size + } else { + hasDeselected = true + } + } + } + + var contentHeight: CGFloat = 0.0 + + var validKeys = Set() + for i in 0 ..< component.categories.count { + let category = component.categories[i] + validKeys.insert(category.key) + + var itemTransition = transition + let itemView: ComponentView + if let current = self.itemViews[category.key] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + itemView.parentState = state + self.itemViews[category.key] = itemView + } + + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(StorageCategoryItemComponent( + theme: component.theme, + strings: component.strings, + category: category, + isExpandedLevel: false, + isExpanded: expandedCategory == category.key, + hasNext: i != component.categories.count - 1, + action: { [weak self] key, actionType in + guard let self, let component = self.component else { + return + } + + switch actionType { + case .generic: + if let category = component.categories.first(where: { $0.key == key }), !category.subcategories.isEmpty { + component.toggleOtherExpanded() + } else { + component.toggleCategorySelection(key) + } + case .toggle: + component.toggleCategorySelection(key) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + + contentHeight += itemSize.height + } + + var removeKeys: [StorageUsageScreenComponent.Category] = [] + for (key, itemView) in self.itemViews { + if !validKeys.contains(key) { + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + removeKeys.append(key) + } + } + for key in removeKeys { + self.itemViews.removeValue(forKey: key) + } + + let clearTitle: String + let label: String? + if totalSelectedSize == 0 { + clearTitle = component.strings.StorageManagement_ClearSelected + label = nil + } else if hasDeselected { + clearTitle = component.strings.StorageManagement_ClearSelected + label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")) + } else { + clearTitle = component.strings.StorageManagement_ClearAll + label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")) + } + + contentHeight += 8.0 + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: clearTitle, + label: label, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: component.theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: component.theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + isEnabled: totalSelectedSize != 0, + animationName: nil, + iconPosition: .right, + iconSpacing: 4.0, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.clearAction() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: contentHeight), size: buttonSize) + if let buttonView = button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + + contentHeight += 16.0 + + self.backgroundColor = component.theme.list.itemBlocksBackgroundColor + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift new file mode 100644 index 00000000000..b08dedad44b --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift @@ -0,0 +1,407 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode + +final class StorageCategoryItemComponent: Component { + enum ActionType { + case toggle + case generic + } + + let theme: PresentationTheme + let strings: PresentationStrings + let category: StorageCategoriesComponent.CategoryData + let isExpandedLevel: Bool + let isExpanded: Bool + let hasNext: Bool + let action: (StorageUsageScreenComponent.Category, ActionType) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + category: StorageCategoriesComponent.CategoryData, + isExpandedLevel: Bool, + isExpanded: Bool, + hasNext: Bool, + action: @escaping (StorageUsageScreenComponent.Category, ActionType) -> Void + ) { + self.theme = theme + self.strings = strings + self.category = category + self.isExpandedLevel = isExpandedLevel + self.isExpanded = isExpanded + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: StorageCategoryItemComponent, rhs: StorageCategoryItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.category != rhs.category { + return false + } + if lhs.isExpandedLevel != rhs.isExpandedLevel { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + class View: HighlightTrackingButton { + private let checkLayer: CheckLayer + private let title = ComponentView() + private let titleValue = ComponentView() + private let label = ComponentView() + private var iconView: UIImageView? + private let separatorLayer: SimpleLayer + + private let checkButtonArea: HighlightTrackingButton + + private let subcategoryClippingContainer: UIView + private var itemViews: [StorageUsageScreenComponent.Category: ComponentView] = [:] + + private var component: StorageCategoryItemComponent? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + override init(frame: CGRect) { + self.checkLayer = CheckLayer() + self.separatorLayer = SimpleLayer() + + self.checkButtonArea = HighlightTrackingButton() + + self.subcategoryClippingContainer = UIView() + self.subcategoryClippingContainer.clipsToBounds = true + + super.init(frame: frame) + + self.addSubview(self.subcategoryClippingContainer) + + self.layer.addSublayer(self.separatorLayer) + self.layer.addSublayer(self.checkLayer) + + self.addSubview(self.checkButtonArea) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.checkButtonArea.addTarget(self, action: #selector(self.checkPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(component.category.key, .generic) + } + + @objc private func checkPressed() { + guard let component = self.component else { + return + } + component.action(component.category.key, .toggle) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result === self.subcategoryClippingContainer { + return self + } + return result + } + + func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color + + self.component = component + + var leftInset: CGFloat = 62.0 + var additionalLeftInset: CGFloat = 0.0 + + if component.isExpandedLevel { + additionalLeftInset += 45.0 + } + leftInset += additionalLeftInset + + let rightInset: CGFloat = 16.0 + + var availableWidth: CGFloat = availableSize.width - leftInset - rightInset + + if !component.category.subcategories.isEmpty { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + if themeUpdated { + iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme) + } + } else { + iconView = UIImageView() + iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme) + self.iconView = iconView + self.addSubview(iconView) + } + + if let image = iconView.image { + availableWidth -= image.size.width + 6.0 + transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: image.size)) + } + } else if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + } + + let fractionValue: Double = floor(component.category.sizeFraction * 100.0 * 10.0) / 10.0 + let fractionString: String + if fractionValue < 0.1 { + fractionString = "<0.1" + } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { + fractionString = "\(Int(fractionValue))" + } else { + fractionString = "\(fractionValue)" + } + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: dataSizeString(Int(component.category.size), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")), font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + availableWidth = max(1.0, availableWidth - labelSize.width - 1.0) + + let titleValueSize = self.titleValue.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "\(fractionString)%", font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + availableWidth = max(1.0, availableWidth - titleValueSize.width - 4.0) + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.category.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)))), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + + var height: CGFloat = 44.0 + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + let titleValueFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - titleValueSize.height) / 2.0)), size: titleValueSize) + + var labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize) + + if let iconView = self.iconView, let image = iconView.image { + labelFrame.origin.x -= image.size.width - 6.0 + + transition.setPosition(view: iconView, position: CGPoint(x: availableSize.width - rightInset + 6.0 - floor(image.size.width * 0.5), y: floor(height * 0.5))) + let angle: CGFloat = component.isExpanded ? CGFloat.pi : 0.0 + transition.setTransform(view: iconView, transform: CATransform3DMakeRotation(CGFloat.pi * 0.5 - angle, 0.0, 0.0, 1.0)) + } + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + if let titleValueView = self.titleValue.view { + if titleValueView.superview == nil { + titleValueView.isUserInteractionEnabled = false + self.addSubview(titleValueView) + } + transition.setFrame(view: titleValueView, frame: titleValueFrame) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: labelFrame) + } + + var copyCheckLayer: CheckLayer? + if themeUpdated { + if !transition.animation.isImmediate { + let copyLayer = CheckLayer(theme: self.checkLayer.theme) + copyLayer.frame = self.checkLayer.frame + copyLayer.setSelected(self.checkLayer.selected, animated: false) + self.layer.addSublayer(copyLayer) + copyCheckLayer = copyLayer + transition.setAlpha(layer: copyLayer, alpha: 0.0, completion: { [weak copyLayer] _ in + copyLayer?.removeFromSuperlayer() + }) + self.checkLayer.opacity = 0.0 + transition.setAlpha(layer: self.checkLayer, alpha: 1.0) + } + + self.checkLayer.theme = CheckNodeTheme( + backgroundColor: component.category.color, + strokeColor: component.theme.list.itemCheckColors.foregroundColor, + borderColor: component.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + } + + let checkDiameter: CGFloat = 22.0 + let checkFrame = CGRect(origin: CGPoint(x: titleFrame.minX - 20.0 - checkDiameter, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter)) + transition.setFrame(layer: self.checkLayer, frame: checkFrame) + + if let copyCheckLayer { + transition.setFrame(layer: copyCheckLayer, frame: checkFrame) + } + + transition.setFrame(view: self.checkButtonArea, frame: CGRect(origin: CGPoint(x: additionalLeftInset, y: 0.0), size: CGSize(width: leftInset - additionalLeftInset, height: height))) + + if self.checkLayer.selected != component.category.isSelected { + self.checkLayer.setSelected(component.category.isSelected, animated: !transition.animation.isImmediate) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + + transition.setAlpha(layer: self.separatorLayer, alpha: (component.isExpanded || component.hasNext) ? 1.0 : 0.0) + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0))) + + var validKeys = Set() + if component.isExpanded { + for i in 0 ..< component.category.subcategories.count { + let category = component.category.subcategories[i] + validKeys.insert(category.key) + + var itemTransition = transition + let itemView: ComponentView + if let current = self.itemViews[category.key] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.itemViews[category.key] = itemView + } + + itemView.parentState = state + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(StorageCategoryItemComponent( + theme: component.theme, + strings: component.strings, + category: category, + isExpandedLevel: true, + isExpanded: false, + hasNext: i != component.category.subcategories.count - 1, + action: { [weak self] key, _ in + guard let self else { + return + } + self.component?.action(key, .toggle) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: height), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.subcategoryClippingContainer.addSubview(itemComponentView) + if !transition.animation.isImmediate { + itemComponentView.alpha = 0.0 + transition.setAlpha(view: itemComponentView, alpha: 1.0) + } + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + + height += itemSize.height + } + } + + var removeKeys: [StorageUsageScreenComponent.Category] = [] + for (key, itemView) in self.itemViews { + if !validKeys.contains(key) { + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + removeKeys.append(key) + } + } + for key in removeKeys { + self.itemViews.removeValue(forKey: key) + } + + transition.setFrame(view: self.subcategoryClippingContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift new file mode 100644 index 00000000000..8a6ff597e90 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -0,0 +1,999 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode +import AvatarNode +import PhotoResources +import SemanticStatusNode + +private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) + +private let redColors: (UInt32, UInt32) = (0xed6b7b, 0xe63f45) +private let greenColors: (UInt32, UInt32) = (0x99de6f, 0x5fb84f) +private let blueColors: (UInt32, UInt32) = (0x72d5fd, 0x2a9ef1) +private let yellowColors: (UInt32, UInt32) = (0xffa24b, 0xed705c) + +private let extensionColorsMap: [String: (UInt32, UInt32)] = [ + "ppt": redColors, + "pptx": redColors, + "pdf": redColors, + "key": redColors, + + "xls": greenColors, + "xlsx": greenColors, + "csv": greenColors, + + "zip": yellowColors, + "rar": yellowColors, + "gzip": yellowColors, + "ai": yellowColors +] + +private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.beginPath() + let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ") + context.clip() + + let gradientColors = [UIColor(rgb: colors.0).cgColor, UIColor(rgb: colors.1).cgColor] as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context.restoreGState() + + context.beginPath() + let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ") + context.clip() + + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.2).cgColor) + context.translateBy(x: 40.0 - 14.0, y: 0.0) + let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ") + }) +} + +private func extensionImage(fileExtension: String?) -> UIImage? { + let colors: (UInt32, UInt32) + if let fileExtension = fileExtension { + if let extensionColors = extensionColorsMap[fileExtension] { + colors = extensionColors + } else { + colors = blueColors + } + } else { + colors = blueColors + } + + if let cachedImage = (extensionImageCache.with { dict in + return dict[colors.0] + }) { + return cachedImage + } else if let image = generateExtensionImage(colors: colors) { + let _ = extensionImageCache.modify { dict in + var dict = dict + dict[colors.0] = image + return dict + } + return image + } else { + return nil + } +} +private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold) +private let mediumExtensionFont = Font.with(size: 14.0, design: .round, weight: .bold) +private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: .bold) + +private final class FileListItemComponent: Component { + enum Icon: Equatable { + case fileExtension(String) + case media(Media, TelegramMediaImageRepresentation) + case audio + + static func ==(lhs: Icon, rhs: Icon) -> Bool { + switch lhs { + case let .fileExtension(value): + if case .fileExtension(value) = rhs { + return true + } else { + return false + } + case let .media(media, representation): + if case let .media(rhsMedia, rhsRepresentation) = rhs { + if media.id != rhsMedia.id { + return false + } + if representation != rhsRepresentation { + return false + } + return true + } else { + return false + } + case .audio: + if case .audio = rhs { + return true + } else { + return false + } + } + } + } + + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let messageId: EngineMessage.Id + let title: String + let subtitle: String + let label: String + let icon: Icon + let sideInset: CGFloat + let selectionState: SelectionState + let hasNext: Bool + let action: (EngineMessage.Id) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + messageId: EngineMessage.Id, + title: String, + subtitle: String, + label: String, + icon: Icon, + sideInset: CGFloat, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EngineMessage.Id) -> Void + ) { + self.context = context + self.theme = theme + self.messageId = messageId + self.title = title + self.subtitle = subtitle + self.label = label + self.icon = icon + self.sideInset = sideInset + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: FileListItemComponent, rhs: FileListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.messageId != rhs.messageId { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let title = ComponentView() + private let subtitle = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + + private var iconView: UIImageView? + private var iconText: ComponentView? + + private var iconImageNode: TransformImageNode? + + private var semanticStatusNode: SemanticStatusNode? + + private var checkLayer: CheckLayer? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: FileListItemComponent? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(component.messageId) + } + + func update(component: FileListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + + let spacing: CGFloat = 1.0 + let height: CGFloat = 52.0 + var leftInset: CGFloat = 62.0 + component.sideInset + var iconLeftInset: CGFloat = component.sideInset + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + iconLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let rightInset: CGFloat = 16.0 + component.sideInset + + if case let .fileExtension(text) = component.icon { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + self.iconView = iconView + self.addSubview(iconView) + } + + let iconText: ComponentView + if let current = self.iconText { + iconText = current + } else { + iconText = ComponentView() + self.iconText = iconText + } + + if themeUpdated { + iconView.image = extensionImage(fileExtension: "mp3") + } + if let image = iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor(( leftInset - iconLeftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + transition.setFrame(view: iconView, frame: iconFrame) + + let iconTextSize = iconText.update( + transition: .immediate, + component: AnyComponent(Text( + text: text, + font: text.count > 3 ? mediumExtensionFont : extensionFont, + color: .white + )), + environment: {}, + containerSize: CGSize(width: iconFrame.width - 4.0, height: 100.0) + ) + if let iconTextView = iconText.view { + if iconTextView.superview == nil { + self.addSubview(iconTextView) + } + transition.setFrame(view: iconTextView, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - iconTextSize.width) / 2.0), y: iconFrame.maxY - iconTextSize.height - 4.0), size: iconTextSize)) + } + } + } else { + if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + } + if let iconText = self.iconText { + self.iconText = nil + iconText.view?.removeFromSuperview() + } + } + + if case let .media(media, representation) = component.icon { + var resetImage = false + + let iconImageNode: TransformImageNode + if let current = self.iconImageNode { + iconImageNode = current + } else { + resetImage = true + + iconImageNode = TransformImageNode() + self.iconImageNode = iconImageNode + self.addSubview(iconImageNode.view) + } + + let iconSize = CGSize(width: 40.0, height: 40.0) + let imageSize: CGSize = representation.dimensions.cgSize + + if resetImage { + if let file = media as? TelegramMediaFile { + iconImageNode.setSignal(chatWebpageSnippetFile( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + mediaReference: FileMediaReference.standalone(media: file).abstract, + representation: representation, + automaticFetch: false + )) + } else if let image = media as? TelegramMediaImage { + iconImageNode.setSignal(mediaGridMessagePhoto( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + photoReference: ImageMediaReference.standalone(media: image), + automaticFetch: false + )) + } + } + + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize) + transition.setFrame(view: iconImageNode.view, frame: iconFrame) + + let iconImageLayout = iconImageNode.asyncLayout() + let iconImageApply = iconImageLayout(TransformImageArguments( + corners: ImageCorners(radius: 8.0), + imageSize: imageSize, + boundingSize: iconSize, + intrinsicInsets: UIEdgeInsets() + )) + iconImageApply() + } else { + if let iconImageNode = self.iconImageNode { + self.iconImageNode = nil + iconImageNode.view.removeFromSuperview() + } + } + + if case .audio = component.icon { + let semanticStatusNode: SemanticStatusNode + if let current = self.semanticStatusNode { + semanticStatusNode = current + } else { + semanticStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) + self.semanticStatusNode = semanticStatusNode + self.addSubview(semanticStatusNode.view) + } + + let iconSize = CGSize(width: 40.0, height: 40.0) + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize) + transition.setFrame(view: semanticStatusNode.view, frame: iconFrame) + + semanticStatusNode.backgroundNodeColor = component.theme.list.itemCheckColors.fillColor + semanticStatusNode.foregroundNodeColor = component.theme.list.itemCheckColors.foregroundColor + semanticStatusNode.overlayForegroundNodeColor = .white + semanticStatusNode.transitionToState(.play) + } else { + if let semanticStatusNode = self.semanticStatusNode { + self.semanticStatusNode = nil + semanticStatusNode.view.removeFromSuperview() + } + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(16.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + + let contentHeight = titleSize.height + spacing + subtitleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - contentHeight) / 2.0)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + spacing), size: subtitleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StorageFileListPanelComponent: Component { + typealias EnvironmentType = StorageUsagePanelEnvironment + + final class Item: Equatable { + let message: Message + let size: Int64 + + init( + message: Message, + size: Int64 + ) { + self.message = message + self.size = size + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.message.id != rhs.message.id { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + final class Items: Equatable { + let items: [Item] + + init(items: [Item]) { + self.items = items + } + + static func ==(lhs: Items, rhs: Items) -> Bool { + if lhs === rhs { + return true + } + return lhs.items == rhs.items + } + } + + let context: AccountContext + let items: Items? + let selectionState: StorageUsageScreenComponent.SelectionState? + let peerAction: (EngineMessage.Id) -> Void + + init( + context: AccountContext, + items: Items?, + selectionState: StorageUsageScreenComponent.SelectionState?, + peerAction: @escaping (EngineMessage.Id) -> Void + ) { + self.context = context + self.items = items + self.selectionState = selectionState + self.peerAction = peerAction + } + + static func ==(lhs: StorageFileListPanelComponent, rhs: StorageFileListPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + return true + } + + private struct ItemLayout: Equatable { + let containerInsets: UIEdgeInsets + let containerWidth: CGFloat + let itemHeight: CGFloat + let itemCount: Int + + let contentHeight: CGFloat + + init( + containerInsets: UIEdgeInsets, + containerWidth: CGFloat, + itemHeight: CGFloat, + itemCount: Int + ) { + self.containerInsets = containerInsets + self.containerWidth = containerWidth + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) + var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) + + let minVisibleIndex = minVisibleRow + let maxVisibleIndex = maxVisibleRow + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + + func itemFrame(for index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private let measureItem = ComponentView() + private var visibleItems: [EngineMessage.Id: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: StorageFileListPanelComponent? + private var environment: StorageUsagePanelEnvironment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: Transition) { + guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0) + + let dataSizeFormatting = DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".") + + var validIds = Set() + if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { + for index in visibleItems.lowerBound ..< visibleItems.upperBound { + if index >= items.items.count { + continue + } + let item = items.items[index] + let id = item.message.id + validIds.insert(id) + + var itemTransition = transition + let itemView: ComponentView + if let current = self.visibleItems[id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.visibleItems[id] = itemView + } + + let itemSelectionState: FileListItemComponent.SelectionState + if let selectionState = component.selectionState { + itemSelectionState = .editing(isSelected: selectionState.selectedMessages.contains(id)) + } else { + itemSelectionState = .none + } + + var isAudio: Bool = false + var isVoice: Bool = false + + let _ = isAudio + let _ = isVoice + + var title: String = environment.strings.Message_File + + var subtitle = stringForFullDate(timestamp: item.message.timestamp, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) + + var extensionIconValue: String? + var imageIconValue: FileListItemComponent.Icon? + + for media in item.message.media { + if let file = media as? TelegramMediaFile { + let isInstantVideo = file.isInstantVideo + + for attribute in file.attributes { + if case let .Audio(voice, duration, titleValue, performer, _) = attribute { + let _ = duration + + isAudio = true + isVoice = voice + + title = titleValue ?? (file.fileName ?? "Unknown Track") + + if let performer = performer { + subtitle = performer + //descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)" + } + + if !voice { + if file.fileName?.lowercased().hasSuffix(".ogg") == true { + //iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: false))) + } else { + //iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: false))) + } + } else { + title = environment.strings.Message_Audio + } + } + } + + if isInstantVideo || isVoice { + /*var authorName: String + if let author = message.forwardInfo?.author { + if author.id == item.context.account.peerId { + authorName = item.presentationData.strings.DialogList_You + } else { + authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } else if let signature = message.forwardInfo?.authorSignature { + authorName = signature + } else if let author = message.author { + if author.id == item.context.account.peerId { + authorName = item.presentationData.strings.DialogList_You + } else { + authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } else { + authorName = " " + } + + if item.isGlobalSearchResult || item.isDownloadList { + let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) + if authorString.count > 1 { + globalAuthorTitle = authorString.last ?? "" + } + authorName = authorString.first ?? "" + } + + titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) + let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) + var descriptionString: String = "" + if let duration = file.duration { + if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo { + descriptionString = stringForDuration(Int32(duration)) + } else { + descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)" + } + } else { + if !(item.isGlobalSearchResult || item.isDownloadList) { + descriptionString = dateString + } + } + + descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) + iconImage = .roundVideo(file)*/ + } else if !isAudio { + var fileName: String = file.fileName ?? environment.strings.Message_File + if file.isVideo { + fileName = environment.strings.Message_Video + } + title = fileName + + var fileExtension: String? + if let range = fileName.range(of: ".", options: [.backwards]) { + fileExtension = fileName[range.upperBound...].lowercased() + } + extensionIconValue = fileExtension + + if let representation = smallestImageRepresentation(file.previewRepresentations) { + imageIconValue = .media(file, representation) + } + } + } else if let image = media as? TelegramMediaImage { + title = environment.strings.Message_Photo + + if let representation = largestImageRepresentation(image.representations) { + imageIconValue = .media(image, representation) + } + } + } + + var icon: FileListItemComponent.Icon = .fileExtension(" ") + if let imageIconValue { + icon = imageIconValue + } else if isAudio { + icon = .audio + } else if let extensionIconValue { + icon = .fileExtension(extensionIconValue) + } + + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(FileListItemComponent( + context: component.context, + theme: environment.theme, + messageId: item.message.id, + title: title, + subtitle: subtitle, + label: dataSizeString(item.size, formatting: dataSizeFormatting), + icon: icon, + sideInset: environment.containerInsets.left, + selectionState: itemSelectionState, + hasNext: index != items.items.count - 1, + action: component.peerAction + )), + environment: {}, + containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + ) + let itemFrame = itemLayout.itemFrame(for: index) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.scrollView.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + } + + var removeIds: [EngineMessage.Id] = [] + for (id, itemView) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + } + + func update(component: StorageFileListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[StorageUsagePanelEnvironment.self].value + self.environment = environment + + let measureItemSize = self.measureItem.update( + transition: .immediate, + component: AnyComponent(FileListItemComponent( + context: component.context, + theme: environment.theme, + messageId: EngineMessage.Id(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), namespace: 0, id: 0), + title: "ABCDEF", + subtitle: "ABCDEF", + label: "1000", + icon: .fileExtension("file"), + sideInset: environment.containerInsets.left, + selectionState: .none, + hasNext: false, + action: { _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + let itemLayout = ItemLayout( + containerInsets: environment.containerInsets, + containerWidth: availableSize.width, + itemHeight: measureItemSize.height, + itemCount: component.items?.items.count ?? 0 + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + var scrollBounds = self.scrollView.bounds + scrollBounds.size = availableSize + if !environment.isScrollable { + scrollBounds.origin = CGPoint() + } + transition.setBounds(view: self.scrollView, bounds: scrollBounds) + self.scrollView.isScrollEnabled = environment.isScrollable + let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.scrollIndicatorInsets = environment.containerInsets + if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift new file mode 100644 index 00000000000..95c6c5623ea --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift @@ -0,0 +1,208 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import CheckNode +import SolidRoundedButtonComponent +import LegacyComponents + +private func stringForCacheSize(strings: PresentationStrings, size: Int32) -> String { + if size > 100 { + return strings.Cache_NoLimit + } else { + return dataSizeString(Int64(size) * 1024 * 1024 * 1024, formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: ".")) + } +} + +private func totalDiskSpace() -> Int64 { + do { + let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) + return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0 + } catch { + return 0 + } +} + +private let maximumCacheSizeValues: [Int32] = { + let diskSpace = totalDiskSpace() + if diskSpace > 100 * 1024 * 1024 * 1024 { + return [5, 20, 50, Int32.max] + } else if diskSpace > 50 * 1024 * 1024 * 1024 { + return [5, 16, 32, Int32.max] + } else if diskSpace > 24 * 1024 * 1024 * 1024 { + return [2, 8, 16, Int32.max] + } else { + return [1, 4, 8, Int32.max] + } +}() + +final class StorageKeepSizeComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + let updateValue: (Int32) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + value: Int32, + updateValue: @escaping (Int32) -> Void + ) { + self.theme = theme + self.strings = strings + self.value = value + self.updateValue = updateValue + } + + static func ==(lhs: StorageKeepSizeComponent, rhs: StorageKeepSizeComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + class View: UIView { + private let titles: [ComponentView] + private var sliderView: TGPhotoEditorSliderView? + + private var component: StorageKeepSizeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.titles = (0 ..< 4).map { _ in ComponentView() } + + super.init(frame: frame) + + self.clipsToBounds = true + self.layer.cornerRadius = 10.0 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageKeepSizeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + + let height: CGFloat = 88.0 + + var titleSizes: [CGSize] = [] + for i in 0 ..< self.titles.count { + let titleSize = self.titles[i].update( + transition: .immediate, + component: AnyComponent(Text(text: stringForCacheSize(strings: component.strings, size: maximumCacheSizeValues[i]), font: Font.regular(13.0), color: component.theme.list.itemSecondaryTextColor)), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + titleSizes.append(titleSize) + } + + let delta = (availableSize.width - 18.0 * 2.0) / CGFloat(titleSizes.count - 1) + for i in 0 ..< titleSizes.count { + let titleSize = titleSizes[i] + if let titleView = self.titles[i].view { + if titleView.superview == nil { + self.addSubview(titleView) + } + + var position: CGFloat = 18.0 + delta * CGFloat(i) + if i == titleSizes.count - 1 { + position -= titleSize.width + } else if i > 0 { + position -= titleSize.width / 2.0 + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: position, y: 15.0), size: titleSize)) + } + } + + var sliderFirstTime = false + let sliderView: TGPhotoEditorSliderView + if let current = self.sliderView { + sliderView = current + } else { + sliderFirstTime = true + sliderView = TGPhotoEditorSliderView() + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 2.0 + sliderView.lineSize = 4.0 + sliderView.dotSize = 5.0 + sliderView.minimumValue = 0.0 + sliderView.maximumValue = 3.0 + sliderView.startValue = 0.0 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.positionsCount = 4 + sliderView.useLinesForPositions = true + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + self.addSubview(sliderView) + } + + if sliderFirstTime || themeUpdated { + sliderView.backgroundColor = component.theme.list.itemBlocksBackgroundColor + sliderView.backColor = component.theme.list.itemSwitchColors.frameColor + sliderView.startColor = component.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = component.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(component.theme) + } + + transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 15.0, y: 37.0), size: CGSize(width: availableSize.width - 15.0 * 2.0, height: 44.0))) + sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) + + self.updateSliderView() + + return CGSize(width: availableSize.width, height: height) + } + + private func updateSliderView() { + guard let sliderView = self.sliderView, let component = self.component else { + return + } + sliderView.maximumValue = 3.0 + sliderView.positionsCount = 4 + + let value = maximumCacheSizeValues.firstIndex(where: { $0 == component.value }) ?? 0 + sliderView.value = CGFloat(value) + } + + @objc private func sliderValueChanged() { + guard let component = self.component, let sliderView = self.sliderView else { + return + } + + let position = Int(sliderView.value) + let value = maximumCacheSizeValues[position] + component.updateValue(value) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift new file mode 100644 index 00000000000..273a130060d --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -0,0 +1,604 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode +import AvatarNode + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +private final class PeerListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + let peer: EnginePeer? + let label: String + let selectionState: SelectionState + let hasNext: Bool + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + sideInset: CGFloat, + title: String, + peer: EnginePeer?, + label: String, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.theme = theme + self.sideInset = sideInset + self.title = title + self.peer = peer + self.label = label + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let avatarNode: AvatarNode + + private var checkLayer: CheckLayer? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: PeerListItemComponent? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.layer.addSublayer(self.avatarNode.layer) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.action(peer) + } + + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + + let height: CGFloat = 52.0 + var leftInset: CGFloat = 62.0 + component.sideInset + var avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + avatarLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let rightInset: CGFloat = 16.0 + component.sideInset + + let avatarSize: CGFloat = 40.0 + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) + } + if let peer = component.peer { + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StoragePeerListPanelComponent: Component { + typealias EnvironmentType = StorageUsagePanelEnvironment + + final class Item: Equatable { + let peer: EnginePeer + let size: Int64 + + init( + peer: EnginePeer, + size: Int64 + ) { + self.peer = peer + self.size = size + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + final class Items: Equatable { + let items: [Item] + + init(items: [Item]) { + self.items = items + } + + static func ==(lhs: Items, rhs: Items) -> Bool { + if lhs === rhs { + return true + } + return lhs.items == rhs.items + } + } + + let context: AccountContext + let items: Items? + let selectionState: StorageUsageScreenComponent.SelectionState? + let peerAction: (EnginePeer) -> Void + + init( + context: AccountContext, + items: Items?, + selectionState: StorageUsageScreenComponent.SelectionState?, + peerAction: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.items = items + self.selectionState = selectionState + self.peerAction = peerAction + } + + static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + return true + } + + private struct ItemLayout: Equatable { + let containerInsets: UIEdgeInsets + let containerWidth: CGFloat + let itemHeight: CGFloat + let itemCount: Int + + let contentHeight: CGFloat + + init( + containerInsets: UIEdgeInsets, + containerWidth: CGFloat, + itemHeight: CGFloat, + itemCount: Int + ) { + self.containerInsets = containerInsets + self.containerWidth = containerWidth + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) + var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) + + let minVisibleIndex = minVisibleRow + let maxVisibleIndex = maxVisibleRow + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + + func itemFrame(for index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private let measureItem = ComponentView() + private var visibleItems: [EnginePeer.Id: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: StoragePeerListPanelComponent? + private var environment: StorageUsagePanelEnvironment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: Transition) { + guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0) + + let dataSizeFormatting = DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".") + + var validIds = Set() + if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { + for index in visibleItems.lowerBound ..< visibleItems.upperBound { + if index >= items.items.count { + continue + } + let item = items.items[index] + let id = item.peer.id + validIds.insert(id) + + var itemTransition = transition + let itemView: ComponentView + if let current = self.visibleItems[id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.visibleItems[id] = itemView + } + + let itemSelectionState: PeerListItemComponent.SelectionState + if let selectionState = component.selectionState { + itemSelectionState = .editing(isSelected: selectionState.selectedPeers.contains(id)) + } else { + itemSelectionState = .none + } + + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: environment.containerInsets.left, + title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: item.peer, + label: dataSizeString(item.size, formatting: dataSizeFormatting), + selectionState: itemSelectionState, + hasNext: index != items.items.count - 1, + action: component.peerAction + )), + environment: {}, + containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + ) + let itemFrame = itemLayout.itemFrame(for: index) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.scrollView.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + } + + var removeIds: [EnginePeer.Id] = [] + for (id, itemView) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + } + + func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[StorageUsagePanelEnvironment.self].value + self.environment = environment + + let measureItemSize = self.measureItem.update( + transition: .immediate, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: environment.containerInsets.left, + title: "ABCDEF", + peer: nil, + label: "1000", + selectionState: .none, + hasNext: false, + action: { _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + let itemLayout = ItemLayout( + containerInsets: environment.containerInsets, + containerWidth: availableSize.width, + itemHeight: measureItemSize.height, + itemCount: component.items?.items.count ?? 0 + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + var scrollBounds = self.scrollView.bounds + scrollBounds.size = availableSize + if !environment.isScrollable { + scrollBounds.origin = CGPoint() + } + transition.setBounds(view: self.scrollView, bounds: scrollBounds) + self.scrollView.isScrollEnabled = environment.isScrollable + let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.scrollIndicatorInsets = environment.containerInsets + if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift new file mode 100644 index 00000000000..b239cf9ddf7 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift @@ -0,0 +1,276 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode + +final class StoragePeerTypeItemComponent: Component { + enum ActionType { + case toggle + case generic + } + + let theme: PresentationTheme + let iconName: String + let title: String + let subtitle: String? + let value: String + let hasNext: Bool + let action: (View) -> Void + + init( + theme: PresentationTheme, + iconName: String, + title: String, + subtitle: String?, + value: String, + hasNext: Bool, + action: @escaping (View) -> Void + ) { + self.theme = theme + self.iconName = iconName + self.title = title + self.subtitle = subtitle + self.value = value + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: StoragePeerTypeItemComponent, rhs: StoragePeerTypeItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + class View: HighlightTrackingButton { + private let iconView: UIImageView + private let title = ComponentView() + private var subtitle: ComponentView? + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let arrowIconView: UIImageView + + private var component: StoragePeerTypeItemComponent? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + var labelView: UIView? { + return self.label.view + } + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + self.iconView = UIImageView() + self.arrowIconView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + + self.addSubview(self.iconView) + self.addSubview(self.arrowIconView) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(self) + } + + func setHasAssociatedMenu(_ hasAssociatedMenu: Bool) { + let transition: Transition + if hasAssociatedMenu { + transition = .immediate + } else { + transition = .easeInOut(duration: 0.25) + } + if let view = self.label.view { + transition.setAlpha(view: view, alpha: hasAssociatedMenu ? 0.5 : 1.0) + } + transition.setAlpha(view: self.arrowIconView, alpha: hasAssociatedMenu ? 0.5 : 1.0) + } + + func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + let leftInset: CGFloat = 62.0 + let rightInset: CGFloat = 32.0 + + var availableWidth: CGFloat = availableSize.width - leftInset - rightInset + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(Text(text: component.value, font: Font.regular(17.0), color: component.theme.list.itemSecondaryTextColor)), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + availableWidth = max(1.0, availableWidth - labelSize.width - 4.0) + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.list.itemPrimaryTextColor)), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + + var subtitleSize: CGSize? + if let subtitleValue = component.subtitle { + let subtitle: ComponentView + if let current = self.subtitle { + subtitle = current + } else { + subtitle = ComponentView() + self.subtitle = subtitle + } + + let subtitleSizeValue = subtitle.update( + transition: transition, + component: AnyComponent(Text(text: subtitleValue, font: Font.regular(15.0), color: component.theme.list.itemSecondaryTextColor)), + environment: {}, + containerSize: CGSize(width: availableWidth, height: 100.0) + ) + subtitleSize = subtitleSizeValue + } else { + if let subtitle = self.subtitle { + self.subtitle = nil + subtitle.view?.removeFromSuperview() + } + } + + var height: CGFloat = 44.0 + if subtitleSize != nil { + height = 60.0 + } + + let titleFrame: CGRect + var subtitleFrame: CGRect? + + if let subtitleSize = subtitleSize { + let spacing: CGFloat = 1.0 + let verticalSize: CGFloat = titleSize.height + subtitleSize.height + spacing + + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalSize) / 2.0)), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + spacing), size: subtitleSize) + } else { + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + } + + let labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + if let subtitleView = self.subtitle?.view, let subtitleFrame { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: labelFrame) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + self.iconView.image = UIImage(bundleImageName: component.iconName) + self.arrowIconView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme) + } + + if let image = self.iconView.image { + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)) + } + if let image = self.arrowIconView.image { + transition.setFrame(view: self.arrowIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset + 5.0, y: floor((height - image.size.height) / 2.0)), size: image.size)) + } + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + transition.setAlpha(layer: self.separatorLayer, alpha: component.hasNext ? 1.0 : 0.0) + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + (component.hasNext ? UIScreenPixel : 0.0))) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift new file mode 100644 index 00000000000..3be178f9198 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -0,0 +1,760 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import TelegramPresentationData + +final class StorageUsagePanelContainerEnvironment: Equatable { + let isScrollable: Bool + + init( + isScrollable: Bool + ) { + self.isScrollable = isScrollable + } + + static func ==(lhs: StorageUsagePanelContainerEnvironment, rhs: StorageUsagePanelContainerEnvironment) -> Bool { + if lhs.isScrollable != rhs.isScrollable { + return false + } + return true + } +} + +final class StorageUsagePanelEnvironment: Equatable { + let theme: PresentationTheme + let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat + let containerInsets: UIEdgeInsets + let isScrollable: Bool + + init( + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + containerInsets: UIEdgeInsets, + isScrollable: Bool + ) { + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.containerInsets = containerInsets + self.isScrollable = isScrollable + } + + static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.containerInsets != rhs.containerInsets { + return false + } + if lhs.isScrollable != rhs.isScrollable { + return false + } + return true + } +} + +private final class StorageUsageHeaderItemComponent: CombinedComponent { + let theme: PresentationTheme + let title: String + let activityFraction: CGFloat + + init( + theme: PresentationTheme, + title: String, + activityFraction: CGFloat + ) { + self.theme = theme + self.title = title + self.activityFraction = activityFraction + } + + static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.activityFraction != rhs.activityFraction { + return false + } + return true + } + + static var body: Body { + let activeText = Child(Text.self) + let inactiveText = Child(Text.self) + + return { context in + let activeText = activeText.update( + component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor), + availableSize: context.availableSize, + transition: .immediate + ) + let inactiveText = inactiveText.update( + component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor), + availableSize: context.availableSize, + transition: .immediate + ) + + context.add(activeText + .position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5)) + .opacity(context.component.activityFraction) + ) + context.add(inactiveText + .position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5)) + .opacity(1.0 - context.component.activityFraction) + ) + + return activeText.size + } + } +} + +private final class StorageUsageHeaderComponent: Component { + struct Item: Equatable { + let id: AnyHashable + let title: String + + init( + id: AnyHashable, + title: String + ) { + self.id = id + self.title = title + } + } + + let theme: PresentationTheme + let items: [Item] + let activeIndex: Int + let transitionFraction: CGFloat + let switchToPanel: (AnyHashable) -> Void + + init( + theme: PresentationTheme, + items: [Item], + activeIndex: Int, + transitionFraction: CGFloat, + switchToPanel: @escaping (AnyHashable) -> Void + ) { + self.theme = theme + self.items = items + self.activeIndex = activeIndex + self.transitionFraction = transitionFraction + self.switchToPanel = switchToPanel + } + + static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.activeIndex != rhs.activeIndex { + return false + } + if lhs.transitionFraction != rhs.transitionFraction { + return false + } + return true + } + + class View: UIView { + private var component: StorageUsageHeaderComponent? + + private var visibleItems: [AnyHashable: ComponentView] = [:] + private let activeItemLayer: SimpleLayer + + override init(frame: CGRect) { + self.activeItemLayer = SimpleLayer() + self.activeItemLayer.cornerRadius = 2.0 + self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + super.init(frame: frame) + + self.layer.addSublayer(self.activeItemLayer) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self) + var closestId: (CGFloat, AnyHashable)? + if self.bounds.contains(point) { + for (id, item) in self.visibleItems { + if let itemView = item.view { + let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX)) + if let closestIdValue = closestId { + if distance < closestIdValue.0 { + closestId = (distance, id) + } + } else { + closestId = (distance, id) + } + } + } + } + if let closestId = closestId, let component = self.component { + component.switchToPanel(closestId.1) + } + } + } + + func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + var validIds = Set() + for i in 0 ..< component.items.count { + let item = component.items[i] + validIds.insert(item.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.visibleItems[item.id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.visibleItems[item.id] = itemView + } + + let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction + let activityDistance: CGFloat = abs(activeIndex - CGFloat(i)) + + let activityFraction: CGFloat + if activityDistance < 1.0 { + activityFraction = 1.0 - activityDistance + } else { + activityFraction = 0.0 + } + + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(StorageUsageHeaderItemComponent( + theme: component.theme, + title: item.title, + activityFraction: activityFraction + )), + environment: {}, + containerSize: availableSize + ) + + let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count) + let itemX: CGFloat + if component.items.count == 1 { + itemX = 37.0 + } else { + itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0) + } + + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.addSubview(itemComponentView) + itemComponentView.isUserInteractionEnabled = false + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + + if component.activeIndex < component.items.count { + let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view + let nextIndex: Int + if component.transitionFraction > 0.0 { + nextIndex = max(0, component.activeIndex - 1) + } else { + nextIndex = min(component.items.count - 1, component.activeIndex + 1) + } + let nextView = self.visibleItems[component.items[nextIndex].id]?.view + if let activeView = activeView, let nextView = nextView { + let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction)) + transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0))) + } + } + + if themeUpdated { + self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + itemComponentView.removeFromSuperview() + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StorageUsagePanelContainerComponent: Component { + typealias EnvironmentType = StorageUsagePanelContainerEnvironment + + struct Item: Equatable { + let id: AnyHashable + let title: String + let panel: AnyComponent + + init( + id: AnyHashable, + title: String, + panel: AnyComponent + ) { + self.id = id + self.title = title + self.panel = panel + } + } + + let theme: PresentationTheme + let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat + let insets: UIEdgeInsets + let items: [Item] + + init( + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + insets: UIEdgeInsets, + items: [Item] + ) { + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.insets = insets + self.items = items + } + + static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + class View: UIView, UIGestureRecognizerDelegate { + private let topPanelBackgroundView: UIView + private let topPanelMergedBackgroundView: UIView + private let topPanelSeparatorLayer: SimpleLayer + private let header = ComponentView() + + private var component: StorageUsagePanelContainerComponent? + private weak var state: EmptyComponentState? + + private let panelsBackgroundLayer: SimpleLayer + private var visiblePanels: [AnyHashable: ComponentView] = [:] + private var actualVisibleIds = Set() + private var currentId: AnyHashable? + private var transitionFraction: CGFloat = 0.0 + private var animatingTransition: Bool = false + + override init(frame: CGRect) { + self.topPanelBackgroundView = UIView() + + self.topPanelMergedBackgroundView = UIView() + self.topPanelMergedBackgroundView.alpha = 0.0 + + self.topPanelSeparatorLayer = SimpleLayer() + + self.panelsBackgroundLayer = SimpleLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.panelsBackgroundLayer) + self.addSubview(self.topPanelBackgroundView) + self.addSubview(self.topPanelMergedBackgroundView) + self.layer.addSublayer(self.topPanelSeparatorLayer) + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in + guard let self, let component = self.component, let currentId = self.currentId else { + return [] + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return [] + } + + /*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { + return [] + }*/ + + if index == 0 { + return .left + } + return [.left, .right] + }) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.addGestureRecognizer(panRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } + } + + cancelContextGestures(view: self) + + //self.animatingTransition = true + case .changed: + guard let component = self.component, let currentId = self.currentId else { + return + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return + } + + let translation = recognizer.translation(in: self) + var transitionFraction = translation.x / self.bounds.width + if index <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if index >= component.items.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + self.state?.updated(transition: .immediate) + + // let nextKey = availablePanes[updatedIndex] + // print(transitionFraction) + //self.paneTransitionPromise.set(transitionFraction) + + //self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + //self.currentPaneUpdated?(false) + case .cancelled, .ended: + guard let component = self.component, let currentId = self.currentId else { + return + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return + } + + let translation = recognizer.translation(in: self) + let velocity = recognizer.velocity(in: self) + var directionIsToRight: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + if abs(translation.x) > self.bounds.width / 2.0 { + directionIsToRight = translation.x > self.bounds.width / 2.0 + } + } + if let directionIsToRight = directionIsToRight { + var updatedIndex = index + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, component.items.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + self.currentId = component.items[updatedIndex].id + } + self.transitionFraction = 0.0 + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + + self.animatingTransition = false + //self.currentPaneUpdated?(false) + + //self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) + default: + break + } + } + + func updateNavigationMergeFactor(value: CGFloat, transition: Transition) { + transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value) + transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) + } + + func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[StorageUsagePanelContainerEnvironment.self].value + + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + if themeUpdated { + self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor + self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor + self.topPanelMergedBackgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor + } + + let topPanelCoverHeight: CGFloat = 10.0 + + let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) + transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) + transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame) + + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) + + transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { + self.currentId = nil + } + if self.currentId == nil { + self.currentId = component.items.first?.id + } + + var visibleIds = Set() + var currentIndex: Int? + if let currentId = self.currentId { + visibleIds.insert(currentId) + + if let index = component.items.firstIndex(where: { $0.id == currentId }) { + currentIndex = index + if index != 0 { + visibleIds.insert(component.items[index - 1].id) + } + if index != component.items.count - 1 { + visibleIds.insert(component.items[index + 1].id) + } + } + } + + let _ = self.header.update( + transition: transition, + component: AnyComponent(StorageUsageHeaderComponent( + theme: component.theme, + items: component.items.map { item -> StorageUsageHeaderComponent.Item in + return StorageUsageHeaderComponent.Item( + id: item.id, + title: item.title + ) + }, + activeIndex: currentIndex ?? 0, + transitionFraction: self.transitionFraction, + switchToPanel: { [weak self] id in + guard let self, let component = self.component else { + return + } + if component.items.contains(where: { $0.id == id }) { + self.currentId = id + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + } + )), + environment: {}, + containerSize: topPanelFrame.size + ) + if let headerView = self.header.view { + if headerView.superview == nil { + self.addSubview(headerView) + } + transition.setFrame(view: headerView, frame: topPanelFrame) + } + + let childEnvironment = StorageUsagePanelEnvironment( + theme: component.theme, + strings: component.strings, + dateTimeFormat: component.dateTimeFormat, + containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), + isScrollable: environment.isScrollable + ) + + let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) + + if self.animatingTransition { + visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil }) + } + + self.actualVisibleIds = visibleIds + + for (id, _) in self.visiblePanels { + visibleIds.insert(id) + } + + var validIds = Set() + if let currentIndex { + var anyAnchorOffset: CGFloat = 0.0 + for (id, panel) in self.visiblePanels { + guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else { + continue + } + var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0) + if itemIndex < currentIndex { + itemFrame.origin.x -= itemFrame.width + } else if itemIndex > currentIndex { + itemFrame.origin.x += itemFrame.width + } + + anyAnchorOffset = itemFrame.minX - panelView.frame.minX + + break + } + + for id in visibleIds { + guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else { + continue + } + let panelItem = component.items[itemIndex] + + var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0) + if itemIndex < currentIndex { + itemFrame.origin.x -= itemFrame.width + } else if itemIndex > currentIndex { + itemFrame.origin.x += itemFrame.width + } + + validIds.insert(panelItem.id) + + let panel: ComponentView + var panelTransition = transition + var animateInIfNeeded = false + if let current = self.visiblePanels[panelItem.id] { + panel = current + + if let panelView = panel.view, !panelView.bounds.isEmpty { + var wasHidden = false + if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne { + wasHidden = true + } + var isHidden = false + if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne { + isHidden = true + } + if wasHidden && isHidden { + panelTransition = .immediate + } + } + } else { + panelTransition = .immediate + animateInIfNeeded = true + + panel = ComponentView() + self.visiblePanels[panelItem.id] = panel + } + let _ = panel.update( + transition: panelTransition, + component: panelItem.panel, + environment: { + childEnvironment + }, + containerSize: centralPanelFrame.size + ) + if let panelView = panel.view { + if panelView.superview == nil { + self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + } + + panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in + guard let self else { + return + } + if !self.actualVisibleIds.contains(id) { + if let panel = self.visiblePanels[id] { + self.visiblePanels.removeValue(forKey: id) + panel.view?.removeFromSuperview() + } + } + }) + if animateInIfNeeded && anyAnchorOffset != 0.0 { + transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true) + } + } + } + } + + var removeIds: [AnyHashable] = [] + for (id, panel) in self.visiblePanels { + if !validIds.contains(id) { + removeIds.append(id) + if let panelView = panel.view { + panelView.removeFromSuperview() + } + } + } + for id in removeIds { + self.visiblePanels.removeValue(forKey: id) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift new file mode 100644 index 00000000000..03648eb9991 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -0,0 +1,2636 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import Markdown +import ContextUI +import AnimatedAvatarSetNode +import AvatarNode +import RadialStatusNode +import UndoUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import TelegramStringFormatting + +private extension StorageUsageScreenComponent.Category { + init(_ category: StorageUsageStats.CategoryKey) { + switch category { + case .photos: + self = .photos + case .videos: + self = .videos + case .files: + self = .files + case .music: + self = .music + case .stickers: + self = .stickers + case .avatars: + self = .avatars + case .misc: + self = .misc + } + } +} + +final class StorageUsageScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let makeStorageUsageExceptionsScreen: (CacheStorageSettings.PeerStorageCategory) -> ViewController? + let peer: EnginePeer? + + init( + context: AccountContext, + makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, + peer: EnginePeer? + ) { + self.context = context + self.makeStorageUsageExceptionsScreen = makeStorageUsageExceptionsScreen + self.peer = peer + } + + static func ==(lhs: StorageUsageScreenComponent, rhs: StorageUsageScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + private final class ScrollViewImpl: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + override var contentOffset: CGPoint { + set(value) { + var value = value + if value.y > self.contentSize.height - self.bounds.height { + value.y = max(0.0, self.contentSize.height - self.bounds.height) + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } + } + + private final class AnimationHint { + enum Value { + case firstStatsUpdate + case clearedItems + } + let value: Value + + init(value: Value) { + self.value = value + } + } + + final class SelectionState: Equatable { + let selectedPeers: Set + let selectedMessages: Set + + init( + selectedPeers: Set, + selectedMessages: Set + ) { + self.selectedPeers = selectedPeers + self.selectedMessages = selectedMessages + } + + convenience init() { + self.init( + selectedPeers: Set(), + selectedMessages: Set() + ) + } + + static func ==(lhs: SelectionState, rhs: SelectionState) -> Bool { + if lhs.selectedPeers != rhs.selectedPeers { + return false + } + if lhs.selectedMessages != rhs.selectedMessages { + return false + } + return true + } + + func togglePeer(id: EnginePeer.Id) -> SelectionState { + var selectedPeers = self.selectedPeers + if selectedPeers.contains(id) { + selectedPeers.remove(id) + } else { + selectedPeers.insert(id) + } + + return SelectionState( + selectedPeers: selectedPeers, + selectedMessages: Set() + ) + } + + func toggleMessage(id: EngineMessage.Id) -> SelectionState { + var selectedMessages = self.selectedMessages + if selectedMessages.contains(id) { + selectedMessages.remove(id) + } else { + selectedMessages.insert(id) + } + + return SelectionState( + selectedPeers: Set(), + selectedMessages: selectedMessages + ) + } + } + + enum Category: Hashable { + case photos + case videos + case files + case music + case other + case stickers + case avatars + case misc + + var color: UIColor { + switch self { + case .photos: + return UIColor(rgb: 0x5AC8FA) + case .videos: + return UIColor(rgb: 0x3478F6) + case .files: + return UIColor(rgb: 0x34C759) + case .music: + return UIColor(rgb: 0xFF2D55) + case .other: + return UIColor(rgb: 0xC4C4C6) + case .stickers: + return UIColor(rgb: 0x5856D6) + case .avatars: + return UIColor(rgb: 0xAF52DE) + case .misc: + return UIColor(rgb: 0xFF9500) + } + } + + func title(strings: PresentationStrings) -> String { + switch self { + case .photos: + return strings.StorageManagement_SectionPhotos + case .videos: + return strings.StorageManagement_SectionVideos + case .files: + return strings.StorageManagement_SectionFiles + case .music: + return strings.StorageManagement_SectionMusic + case .other: + return strings.StorageManagement_SectionOther + case .stickers: + return strings.StorageManagement_SectionStickers + case .avatars: + return strings.StorageManagement_SectionAvatars + case .misc: + return strings.StorageManagement_SectionMiscellaneous + } + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollViewImpl + + private var currentStats: AllStorageUsageStats? + private var existingCategories: Set = Set() + + private var currentMessages: [MessageId: Message] = [:] + private var cacheSettings: CacheStorageSettings? + private var cacheSettingsExceptionCount: [CacheStorageSettings.PeerStorageCategory: Int32]? + + private var peerItems: StoragePeerListPanelComponent.Items? + private var imageItems: StorageFileListPanelComponent.Items? + private var fileItems: StorageFileListPanelComponent.Items? + private var musicItems: StorageFileListPanelComponent.Items? + + private var selectionState: SelectionState? + + private var clearingDisplayTimestamp: Double? + private var isClearing: Bool = false { + didSet { + if self.isClearing != oldValue { + if self.isClearing { + if self.keepScreenActiveDisposable == nil { + self.keepScreenActiveDisposable = self.component?.context.sharedContext.applicationBindings.pushIdleTimerExtension() + } + } else { + if let keepScreenActiveDisposable = self.keepScreenActiveDisposable { + self.keepScreenActiveDisposable = nil + keepScreenActiveDisposable.dispose() + } + } + } + } + } + + private var selectedCategories: Set = Set() + private var isOtherCategoryExpanded: Bool = false + + private let navigationBackgroundView: BlurredBackgroundView + private let navigationSeparatorLayer: SimpleLayer + private let navigationSeparatorLayerContainer: SimpleLayer + private let navigationEditButton = ComponentView() + private let navigationDoneButton = ComponentView() + + private let headerView = ComponentView() + private let headerOffsetContainer: UIView + private let headerDescriptionView = ComponentView() + + private let headerProgressBackgroundLayer: SimpleLayer + private let headerProgressForegroundLayer: SimpleLayer + + private var chartAvatarNode: AvatarNode? + + private var doneStatusCircle: SimpleShapeLayer? + private var doneStatusNode: RadialStatusNode? + + private let pieChartView = ComponentView() + private let chartTotalLabel = ComponentView() + private let categoriesView = ComponentView() + private let categoriesDescriptionView = ComponentView() + + private let keepDurationTitleView = ComponentView() + private let keepDurationDescriptionView = ComponentView() + private var keepDurationSectionContainerView: UIView + private var keepDurationItems: [AnyHashable: ComponentView] = [:] + + private let keepSizeTitleView = ComponentView() + private let keepSizeView = ComponentView() + private let keepSizeDescriptionView = ComponentView() + + private let panelContainer = ComponentView() + + private var selectionPanel: ComponentView? + + private var clearingNode: StorageUsageClearProgressOverlayNode? + + private var loadingView: UIActivityIndicatorView? + + private var component: StorageUsageScreenComponent? + private weak var state: EmptyComponentState? + private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? + private var controller: (() -> ViewController?)? + + private var enableVelocityTracking: Bool = false + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + + private var ignoreScrolling: Bool = false + + private var statsDisposable: Disposable? + private var messagesDisposable: Disposable? + private var cacheSettingsDisposable: Disposable? + private var keepScreenActiveDisposable: Disposable? + + override init(frame: CGRect) { + self.headerOffsetContainer = UIView() + self.headerOffsetContainer.isUserInteractionEnabled = false + + self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.navigationBackgroundView.alpha = 0.0 + + self.navigationSeparatorLayer = SimpleLayer() + self.navigationSeparatorLayer.opacity = 0.0 + self.navigationSeparatorLayerContainer = SimpleLayer() + self.navigationSeparatorLayerContainer.opacity = 0.0 + + self.scrollView = ScrollViewImpl() + + self.keepDurationSectionContainerView = UIView() + self.keepDurationSectionContainerView.clipsToBounds = true + self.keepDurationSectionContainerView.layer.cornerRadius = 10.0 + + self.headerProgressBackgroundLayer = SimpleLayer() + self.headerProgressForegroundLayer = SimpleLayer() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.keepDurationSectionContainerView) + + self.scrollView.layer.addSublayer(self.headerProgressBackgroundLayer) + self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer) + + self.addSubview(self.navigationBackgroundView) + + self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) + self.layer.addSublayer(self.navigationSeparatorLayerContainer) + + self.addSubview(self.headerOffsetContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.statsDisposable?.dispose() + self.messagesDisposable?.dispose() + self.keepScreenActiveDisposable?.dispose() + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.enableVelocityTracking = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let _ = self.navigationMetrics else { + return + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.enableVelocityTracking = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View { + let _ = panelContainerView + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + //panelContainerView.transferVelocity(self.previousVelocityM1) + } + } + } + + private func updateScrolling(transition: Transition) { + let scrollBounds = self.scrollView.bounds + + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height + + if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics { + var headerOffset: CGFloat = scrollBounds.minY + + let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) + + let minOffset = headerView.center.y - minY + + headerOffset = min(headerOffset, minOffset) + + let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 + + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + if let navigationEditButtonView = self.navigationEditButton.view { + animatedTransition.setAlpha(view: navigationEditButtonView, alpha: (self.selectionState == nil ? 1.0 : 0.0) * navigationBackgroundAlpha) + } + if let navigationDoneButtonView = self.navigationDoneButton.view { + animatedTransition.setAlpha(view: navigationDoneButtonView, alpha: (self.selectionState == nil ? 0.0 : 1.0) * navigationBackgroundAlpha) + } + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View { + panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) + } + + var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 + offsetFraction = min(1.0, max(0.0, offsetFraction)) + transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction)) + + transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size)) + } + + let _ = self.panelContainer.updateEnvironment( + transition: transition, + environment: { + StorageUsagePanelContainerEnvironment(isScrollable: isLockedAtPanels) + } + ) + } + + func update(component: StorageUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + if self.currentStats == nil { + let loadingView: UIActivityIndicatorView + if let current = self.loadingView { + loadingView = current + } else { + let style: UIActivityIndicatorView.Style + if environment.theme.overallDarkAppearance { + style = .whiteLarge + } else { + if #available(iOS 13.0, *) { + style = .large + } else { + style = .gray + } + } + loadingView = UIActivityIndicatorView(style: style) + self.loadingView = loadingView + loadingView.sizeToFit() + self.insertSubview(loadingView, belowSubview: self.scrollView) + } + let loadingViewSize = loadingView.bounds.size + transition.setFrame(view: loadingView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - loadingViewSize.width) / 2.0), y: floor((availableSize.height - loadingViewSize.height) / 2.0)), size: loadingViewSize)) + if !loadingView.isAnimating { + loadingView.startAnimating() + } + } else { + if let loadingView = self.loadingView { + self.loadingView = nil + loadingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak loadingView] _ in + loadingView?.removeFromSuperview() + }) + } + } + + if self.statsDisposable == nil { + let context = component.context + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = component.context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + } + |> distinctUntilChanged + |> mapToSignal { accountSpecificSettings -> Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> in + return context.engine.data.get( + EngineDataMap(accountSpecificSettings.peerStorageTimeoutExceptions.map(\.key).map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> map { peers -> [CacheStorageSettings.PeerStorageCategory: Int32] in + var result: [CacheStorageSettings.PeerStorageCategory: Int32] = [:] + + for (_, peer) in peers { + guard let peer else { + continue + } + switch peer { + case .user, .secretChat: + result[.privateChats, default: 0] += 1 + case .legacyGroup: + result[.groups, default: 0] += 1 + case let .channel(channel): + if case .group = channel.info { + result[.groups, default: 0] += 1 + } else { + result[.channels, default: 0] += 1 + } + } + } + + return result + } + } + + self.cacheSettingsDisposable = (combineLatest(queue: .mainQueue(), + component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> map { sharedData -> CacheStorageSettings in + let cacheSettings: CacheStorageSettings + if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = CacheStorageSettings.defaultSettings + } + + return cacheSettings + }, + cacheSettingsExceptionCount + ) + |> deliverOnMainQueue).start(next: { [weak self] cacheSettings, cacheSettingsExceptionCount in + guard let self else { + return + } + self.cacheSettings = cacheSettings + self.cacheSettingsExceptionCount = cacheSettingsExceptionCount + if self.currentStats != nil { + self.state?.updated(transition: .immediate) + } + }) + + self.reloadStats(firstTime: true, completion: {}) + } + + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } + } + + let animationHint = transition.userData(AnimationHint.self) + + if let animationHint { + if case .firstStatsUpdate = animationHint.value { + let alphaTransition: Transition = .easeInOut(duration: 0.25) + alphaTransition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) + alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) + } else if case .clearedItems = animationHint.value { + if let snapshotView = self.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } else { + transition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) + transition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) + } + + self.controller = environment.controller + + self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) + + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight)) + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) + + let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + + transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame) + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size)) + + let navigationEditButtonSize = self.navigationEditButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Edit, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState( + selectedPeers: Set(), + selectedMessages: Set() + ) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationEditButtonView = self.navigationEditButton.view { + if navigationEditButtonView.superview == nil { + self.addSubview(navigationEditButtonView) + } + transition.setFrame(view: navigationEditButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - environment.safeInsets.right - navigationEditButtonSize.width, y: environment.statusBarHeight), size: navigationEditButtonSize)) + } + + let navigationDoneButtonSize = self.navigationDoneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self else { + return + } + self.selectionState = nil + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationDoneButtonView = self.navigationDoneButton.view { + if navigationDoneButtonView.superview == nil { + self.addSubview(navigationDoneButtonView) + } + transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize)) + } + + let navigationRightButtonMaxWidth: CGFloat = max(navigationEditButtonSize.width, navigationDoneButtonSize.width) + + self.backgroundColor = environment.theme.list.blocksBackgroundColor + + var contentHeight: CGFloat = 0.0 + + let topInset: CGFloat = 19.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var bottomInset: CGFloat = environment.safeInsets.bottom + if let selectionState = self.selectionState { + let selectionPanel: ComponentView + var selectionPanelTransition = transition + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = .immediate + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + var selectedSize: Int64 = 0 + if let currentStats = self.currentStats { + for peerId in selectionState.selectedPeers { + if let stats = currentStats.peers[peerId] { + let peerSize = stats.stats.categories.values.reduce(0, { + $0 + $1.size + }) + selectedSize += peerSize + } + } + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for messageId in selectionState.selectedMessages { + for (_, category) in contextStats.categories { + if let messageSize = category.messages[messageId] { + selectedSize += messageSize + break + } + } + } + } + + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(StorageUsageScreenSelectionPanelComponent( + theme: environment.theme, + title: environment.strings.StorageManagement_ClearSelected, + label: selectedSize == 0 ? nil : dataSizeString(Int(selectedSize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), + isEnabled: selectedSize != 0, + insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset), + action: { [weak self] in + guard let self, let selectionState = self.selectionState else { + return + } + self.requestClear(categories: Set(), peers: selectionState.selectedPeers, messages: selectionState.selectedMessages) + } + )), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view { + var animateIn = false + if selectionPanelView.superview == nil { + self.addSubview(selectionPanelView) + animateIn = true + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize)) + if animateIn { + transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true) + } + } + bottomInset = selectionPanelSize.height + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view { + transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + + contentHeight += environment.statusBarHeight + topInset + + let chartOrder: [Category] = [ + .photos, + .videos, + .files, + .music, + .stickers, + .avatars, + .misc + ] + + if let _ = self.currentStats { + if let animationHint { + switch animationHint.value { + case .firstStatsUpdate, .clearedItems: + self.selectedCategories = self.existingCategories + } + } + + self.selectedCategories.formIntersection(self.existingCategories) + } else { + self.selectedCategories.removeAll() + } + + var chartItems: [PieChartComponent.ChartData.Item] = [] + var listCategories: [StorageCategoriesComponent.CategoryData] = [] + + let otherCategories: [Category] = [ + .stickers, + .avatars, + .misc + ] + + var totalSize: Int64 = 0 + if let currentStats = self.currentStats { + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for (_, value) in contextStats.categories { + totalSize += value.size + } + + for category in chartOrder { + let mappedCategory: StorageUsageStats.CategoryKey + switch category { + case .photos: + mappedCategory = .photos + case .videos: + mappedCategory = .videos + case .files: + mappedCategory = .files + case .music: + mappedCategory = .music + case .stickers: + mappedCategory = .stickers + case .avatars: + mappedCategory = .avatars + case .misc: + mappedCategory = .misc + case .other: + continue + } + + var categorySize: Int64 = 0 + if let categoryData = contextStats.categories[mappedCategory] { + categorySize = categoryData.size + } + + let categoryFraction: Double + if categorySize == 0 || totalSize == 0 { + categoryFraction = 0.0 + } else { + categoryFraction = Double(categorySize) / Double(totalSize) + } + + var categoryChartFraction: CGFloat = categoryFraction + if !self.selectedCategories.isEmpty && !self.selectedCategories.contains(category) { + categoryChartFraction = 0.0 + } + + var chartCategoryColor = category.color + if !self.isOtherCategoryExpanded && otherCategories.contains(category) { + chartCategoryColor = Category.misc.color + } + + chartItems.append(PieChartComponent.ChartData.Item(id: category, displayValue: categoryFraction, value: categoryChartFraction, color: chartCategoryColor)) + + if categorySize != 0 { + listCategories.append(StorageCategoriesComponent.CategoryData( + key: category, color: category.color, title: category.title(strings: environment.strings), size: categorySize, sizeFraction: categoryFraction, isSelected: self.selectedCategories.contains(category), subcategories: [])) + } + } + } + + var otherListCategories: [StorageCategoriesComponent.CategoryData] = [] + for listCategory in listCategories { + if otherCategories.contains(where: { $0 == listCategory.key }) { + otherListCategories.append(listCategory) + } + } + listCategories = listCategories.filter { item in + return !otherCategories.contains(where: { $0 == item.key }) + } + if !otherListCategories.isEmpty { + var totalOtherSize: Int64 = 0 + for listCategory in otherListCategories { + totalOtherSize += listCategory.size + } + let categoryFraction: Double + if totalOtherSize == 0 || totalSize == 0 { + categoryFraction = 0.0 + } else { + categoryFraction = Double(totalOtherSize) / Double(totalSize) + } + let isSelected = otherListCategories.allSatisfy { item in + return self.selectedCategories.contains(item.key) + } + + let listColor: UIColor + if self.isOtherCategoryExpanded { + listColor = Category.other.color + } else { + listColor = Category.misc.color + } + + listCategories.append(StorageCategoriesComponent.CategoryData( + key: Category.other, color: listColor, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories)) + } + + if !self.isOtherCategoryExpanded { + var otherSum: CGFloat = 0.0 + var otherRealSum: CGFloat = 0.0 + for i in 0 ..< chartItems.count { + if otherCategories.contains(chartItems[i].id) { + var itemValue = chartItems[i].value + if itemValue > 0.00001 { + itemValue = max(itemValue, 0.01) + } + otherSum += itemValue + otherRealSum += chartItems[i].displayValue + if case .misc = chartItems[i].id { + } else { + chartItems[i].value = 0.0 + } + } + } + if let index = chartItems.firstIndex(where: { $0.id == .misc }) { + chartItems[index].value = otherSum + chartItems[index].displayValue = otherRealSum + } + } + + let chartData = PieChartComponent.ChartData(items: chartItems) + self.pieChartView.parentState = state + let pieChartSize = self.pieChartView.update( + transition: transition, + component: AnyComponent(PieChartComponent( + theme: environment.theme, + chartData: chartData + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 60.0) + ) + let pieChartFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: pieChartSize) + if let pieChartComponentView = self.pieChartView.view { + if pieChartComponentView.superview == nil { + self.scrollView.addSubview(pieChartComponentView) + } + + transition.setFrame(view: pieChartComponentView, frame: pieChartFrame) + transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) + } + if let _ = self.currentStats, listCategories.isEmpty { + let checkColor = UIColor(rgb: 0x34C759) + + let doneStatusNode: RadialStatusNode + var animateIn = false + if let current = self.doneStatusNode { + doneStatusNode = current + } else { + doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear) + self.doneStatusNode = doneStatusNode + self.scrollView.addSubnode(doneStatusNode) + animateIn = true + } + let doneSize = CGSize(width: 100.0, height: 100.0) + doneStatusNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize) + + let doneStatusCircle: SimpleShapeLayer + if let current = self.doneStatusCircle { + doneStatusCircle = current + } else { + doneStatusCircle = SimpleShapeLayer() + self.doneStatusCircle = doneStatusCircle + self.scrollView.layer.addSublayer(doneStatusCircle) + doneStatusCircle.opacity = 0.0 + } + + if animateIn { + Queue.mainQueue().after(0.18, { + doneStatusNode.transitionToState(.check(checkColor), animated: true) + doneStatusCircle.opacity = 1.0 + doneStatusCircle.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + }) + } + + doneStatusCircle.lineWidth = 6.0 + doneStatusCircle.strokeColor = checkColor.cgColor + doneStatusCircle.fillColor = nil + doneStatusCircle.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: doneStatusCircle.lineWidth * 0.5, y: doneStatusCircle.lineWidth * 0.5), size: CGSize(width: doneSize.width - doneStatusCircle.lineWidth * 0.5, height: doneSize.height - doneStatusCircle.lineWidth * 0.5))).cgPath + + doneStatusCircle.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize).insetBy(dx: -doneStatusCircle.lineWidth * 0.5, dy: -doneStatusCircle.lineWidth * 0.5) + + contentHeight += doneSize.height + } else { + contentHeight += pieChartSize.height + + if let doneStatusNode = self.doneStatusNode { + self.doneStatusNode = nil + doneStatusNode.removeFromSupernode() + } + if let doneStatusCircle = self.doneStatusCircle { + self.doneStatusCircle = nil + doneStatusCircle.removeFromSuperlayer() + } + } + + contentHeight += 23.0 + + let headerText: String + if listCategories.isEmpty { + headerText = environment.strings.StorageManagement_TitleCleared + } else if let peer = component.peer { + headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + } else { + headerText = environment.strings.StorageManagement_Title + } + let headerViewSize = self.headerView.update( + transition: transition, + component: AnyComponent(Text(text: headerText, font: Font.semibold(20.0), color: environment.theme.list.itemPrimaryTextColor)), + environment: {}, + containerSize: CGSize(width: floor((availableSize.width - navigationRightButtonMaxWidth * 2.0) / 0.8), height: 100.0) + ) + let headerViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerViewSize.width) / 2.0), y: contentHeight), size: headerViewSize) + if let headerComponentView = self.headerView.view { + if headerComponentView.superview == nil { + self.headerOffsetContainer.addSubview(headerComponentView) + } + transition.setPosition(view: headerComponentView, position: headerViewFrame.center) + transition.setBounds(view: headerComponentView, bounds: CGRect(origin: CGPoint(), size: headerViewFrame.size)) + } + contentHeight += headerViewSize.height + + contentHeight += 6.0 + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) + + var usageFraction: Double = 0.0 + let totalUsageText: String + if listCategories.isEmpty { + totalUsageText = environment.strings.StorageManagement_DescriptionCleared + } else if let currentStats = self.currentStats { + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + var totalStatsSize: Int64 = 0 + for (_, value) in contextStats.categories { + totalStatsSize += value.size + } + + if let _ = component.peer { + var allStatsSize: Int64 = 0 + for (_, value) in currentStats.totalStats.categories { + allStatsSize += value.size + } + + let fraction: Double + if allStatsSize != 0 { + fraction = Double(totalStatsSize) / Double(allStatsSize) + } else { + fraction = 0.0 + } + usageFraction = fraction + let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0 + let fractionString: String + if fractionValue < 0.1 { + fractionString = "<0.1" + } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { + fractionString = "\(Int(fractionValue))" + } else { + fractionString = "\(fractionValue)" + } + + totalUsageText = environment.strings.StorageManagement_DescriptionChatUsage(fractionString).string + } else { + let fraction: Double + if currentStats.deviceFreeSpace != 0 && totalStatsSize != 0 { + fraction = Double(totalStatsSize) / Double(currentStats.deviceFreeSpace + totalStatsSize) + } else { + fraction = 0.0 + } + usageFraction = fraction + let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0 + let fractionString: String + if fractionValue < 0.1 { + fractionString = "<0.1" + } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { + fractionString = "\(Int(fractionValue))" + } else { + fractionString = "\(fractionValue)" + } + + totalUsageText = environment.strings.StorageManagement_DescriptionAppUsage(fractionString).string + } + } else { + totalUsageText = " " + } + let headerDescriptionSize = self.headerDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalUsageText, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), horizontalAlignment: .center, maximumNumberOfLines: 0)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let headerDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerDescriptionSize.width) / 2.0), y: contentHeight), size: headerDescriptionSize) + if let headerDescriptionComponentView = self.headerDescriptionView.view { + if headerDescriptionComponentView.superview == nil { + self.scrollView.addSubview(headerDescriptionComponentView) + } + transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame) + } + contentHeight += headerDescriptionSize.height + contentHeight += 8.0 + + let headerProgressWidth: CGFloat = min(200.0, availableSize.width - sideInset * 2.0) + let headerProgressFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerProgressWidth) / 2.0), y: contentHeight), size: CGSize(width: headerProgressWidth, height: 4.0)) + transition.setFrame(layer: self.headerProgressBackgroundLayer, frame: headerProgressFrame) + transition.setCornerRadius(layer: self.headerProgressBackgroundLayer, cornerRadius: headerProgressFrame.height * 0.5) + self.headerProgressBackgroundLayer.backgroundColor = environment.theme.list.itemAccentColor.withMultipliedAlpha(0.2).cgColor + + let headerProgress: CGFloat = usageFraction + transition.setFrame(layer: self.headerProgressForegroundLayer, frame: CGRect(origin: headerProgressFrame.origin, size: CGSize(width: max(headerProgressFrame.height, floorToScreenPixels(headerProgress * headerProgressFrame.width)), height: headerProgressFrame.height))) + transition.setCornerRadius(layer: self.headerProgressForegroundLayer, cornerRadius: headerProgressFrame.height * 0.5) + self.headerProgressForegroundLayer.backgroundColor = environment.theme.list.itemAccentColor.cgColor + contentHeight += 4.0 + + transition.setAlpha(layer: self.headerProgressBackgroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0) + transition.setAlpha(layer: self.headerProgressForegroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0) + + contentHeight += 24.0 + + if let peer = component.peer { + let avatarSize = CGSize(width: 72.0, height: 72.0) + let avatarFrame: CGRect = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - avatarSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - avatarSize.height) / 2.0)), size: avatarSize) + + let chartAvatarNode: AvatarNode + if let current = self.chartAvatarNode { + chartAvatarNode = current + transition.setFrame(view: chartAvatarNode.view, frame: avatarFrame) + } else { + chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) + self.chartAvatarNode = chartAvatarNode + self.scrollView.addSubview(chartAvatarNode.view) + chartAvatarNode.frame = avatarFrame + + chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize) + } + transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0) + } else { + let chartTotalLabelSize = self.chartTotalLabel.update( + transition: transition, + component: AnyComponent(Text(text: dataSizeString(Int(totalSize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), font: Font.with(size: 20.0, design: .round, weight: .bold), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) + ) + if let chartTotalLabelView = self.chartTotalLabel.view { + if chartTotalLabelView.superview == nil { + self.scrollView.addSubview(chartTotalLabelView) + } + transition.setFrame(view: chartTotalLabelView, frame: CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize)) + transition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0) + } + } + + if !listCategories.isEmpty { + self.categoriesView.parentState = state + let categoriesSize = self.categoriesView.update( + transition: transition, + component: AnyComponent(StorageCategoriesComponent( + theme: environment.theme, + strings: environment.strings, + categories: listCategories, + isOtherExpanded: self.isOtherCategoryExpanded, + toggleCategorySelection: { [weak self] key in + guard let self else { + return + } + if key == Category.other { + var otherCategories: [Category] = [.stickers, .avatars, .misc] + otherCategories = otherCategories.filter(self.existingCategories.contains) + if !otherCategories.isEmpty { + if otherCategories.allSatisfy(self.selectedCategories.contains) { + for item in otherCategories { + self.selectedCategories.remove(item) + } + } else { + for item in otherCategories { + let _ = self.selectedCategories.insert(item) + } + } + } + } else { + if self.selectedCategories.contains(key) { + self.selectedCategories.remove(key) + } else { + self.selectedCategories.insert(key) + } + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }, + toggleOtherExpanded: { [weak self] in + guard let self else { + return + } + + self.isOtherCategoryExpanded = !self.isOtherCategoryExpanded + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }, + clearAction: { [weak self] in + guard let self else { + return + } + self.requestClear(categories: self.selectedCategories, peers: Set(), messages: Set()) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) + ) + if let categoriesComponentView = self.categoriesView.view { + if categoriesComponentView.superview == nil { + self.scrollView.addSubview(categoriesComponentView) + } + + transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize)) + } + contentHeight += categoriesSize.height + contentHeight += 8.0 + + + let categoriesDescriptionSize = self.categoriesDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .markdown(text: environment.strings.StorageManagement_SectionsDescription, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), maximumNumberOfLines: 0)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize) + if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { + if categoriesDescriptionComponentView.superview == nil { + self.scrollView.addSubview(categoriesDescriptionComponentView) + } + transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) + } + contentHeight += categoriesDescriptionSize.height + contentHeight += 40.0 + } else { + self.categoriesView.view?.removeFromSuperview() + self.categoriesDescriptionView.view?.removeFromSuperview() + } + + if component.peer == nil { + let keepDurationTitleSize = self.keepDurationTitleView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.StorageManagement_AutoremoveHeader, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize) + if let keepDurationTitleComponentView = self.keepDurationTitleView.view { + if keepDurationTitleComponentView.superview == nil { + self.scrollView.addSubview(keepDurationTitleComponentView) + } + transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame) + } + contentHeight += keepDurationTitleSize.height + contentHeight += 8.0 + + + var keepContentHeight: CGFloat = 0.0 + for i in 0 ..< 3 { + let item: ComponentView + if let current = self.keepDurationItems[i] { + item = current + } else { + item = ComponentView() + self.keepDurationItems[i] = item + } + + let mappedCategory: CacheStorageSettings.PeerStorageCategory + + let iconName: String + let title: String + switch i { + case 0: + iconName = "Settings/Menu/EditProfile" + title = environment.strings.Notifications_PrivateChats + mappedCategory = .privateChats + case 1: + iconName = "Settings/Menu/GroupChats" + title = environment.strings.Notifications_GroupChats + mappedCategory = .groups + default: + iconName = "Settings/Menu/Channels" + title = environment.strings.Notifications_Channels + mappedCategory = .channels + } + + let value = self.cacheSettings?.categoryStorageTimeout[mappedCategory] ?? Int32.max + let optionText: String + if value == Int32.max { + optionText = environment.strings.ClearCache_Never + } else { + optionText = timeIntervalString(strings: environment.strings, value: value) + } + + var subtitle: String? + if let cacheSettingsExceptionCount = self.cacheSettingsExceptionCount, let categoryCount = cacheSettingsExceptionCount[mappedCategory] { + subtitle = environment.strings.CacheEvictionMenu_CategoryExceptions(Int32(categoryCount)) + } + + let itemSize = item.update( + transition: transition, + component: AnyComponent(StoragePeerTypeItemComponent( + theme: environment.theme, + iconName: iconName, + title: title, + subtitle: subtitle, + value: optionText, + hasNext: i != 3 - 1, + action: { [weak self] sourceView in + guard let self else { + return + } + self.openKeepMediaCategory(mappedCategory: mappedCategory, sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: keepContentHeight), size: itemSize) + if let itemView = item.view { + if itemView.superview == nil { + self.keepDurationSectionContainerView.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + keepContentHeight += itemSize.height + } + self.keepDurationSectionContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + transition.setFrame(view: self.keepDurationSectionContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: keepContentHeight))) + contentHeight += keepContentHeight + contentHeight += 8.0 + + let keepDurationDescriptionSize = self.keepDurationDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.StorageManagement_AutoremoveDescription, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize) + if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view { + if keepDurationDescriptionComponentView.superview == nil { + self.scrollView.addSubview(keepDurationDescriptionComponentView) + } + transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame) + } + contentHeight += keepDurationDescriptionSize.height + contentHeight += 40.0 + + let keepSizeTitleSize = self.keepSizeTitleView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.Cache_MaximumCacheSize.uppercased(), attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepSizeTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeTitleSize) + if let keepSizeTitleComponentView = self.keepSizeTitleView.view { + if keepSizeTitleComponentView.superview == nil { + self.scrollView.addSubview(keepSizeTitleComponentView) + } + transition.setFrame(view: keepSizeTitleComponentView, frame: keepSizeTitleFrame) + } + contentHeight += keepSizeTitleSize.height + contentHeight += 8.0 + + let keepSizeSize = self.keepSizeView.update( + transition: transition, + component: AnyComponent(StorageKeepSizeComponent( + theme: environment.theme, + strings: environment.strings, + value: cacheSettings?.defaultCacheStorageLimitGigabytes ?? 32, + updateValue: { [weak self] value in + guard let self, let component = self.component else { + return + } + let value = max(5, value) + let _ = updateCacheStorageSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { current in + var current = current + current.defaultCacheStorageLimitGigabytes = value + return current + }).start() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let keepSizeFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: keepSizeSize) + if let keepSizeComponentView = self.keepSizeView.view { + if keepSizeComponentView.superview == nil { + self.scrollView.addSubview(keepSizeComponentView) + } + transition.setFrame(view: keepSizeComponentView, frame: keepSizeFrame) + } + contentHeight += keepSizeSize.height + contentHeight += 8.0 + + let keepSizeDescriptionSize = self.keepSizeDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.StorageManagement_AutoremoveSpaceDescription, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepSizeDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeDescriptionSize) + if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view { + if keepSizeDescriptionComponentView.superview == nil { + self.scrollView.addSubview(keepSizeDescriptionComponentView) + } + transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame) + } + contentHeight += keepSizeDescriptionSize.height + contentHeight += 40.0 + } + + var panelItems: [StorageUsagePanelContainerComponent.Item] = [] + if let peerItems = self.peerItems, !peerItems.items.isEmpty, !listCategories.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "peers", + title: environment.strings.StorageManagement_TabChats, + panel: AnyComponent(StoragePeerListPanelComponent( + context: component.context, + items: self.peerItems, + selectionState: self.selectionState, + peerAction: { [weak self] peer in + guard let self else { + return + } + if let selectionState = self.selectionState { + self.selectionState = selectionState.togglePeer(id: peer.id) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } else { + self.openPeer(peer: peer) + } + } + )) + )) + } + if let imageItems = self.imageItems, !imageItems.items.isEmpty, !listCategories.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "images", + title: environment.strings.StorageManagement_TabMedia, + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.imageItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState() + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + if let fileItems = self.fileItems, !fileItems.items.isEmpty, !listCategories.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "files", + title: environment.strings.StorageManagement_TabFiles, + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.fileItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState() + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + if let musicItems = self.musicItems, !musicItems.items.isEmpty, !listCategories.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "music", + title: environment.strings.StorageManagement_TabMusic, + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.musicItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState() + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + + if !panelItems.isEmpty { + let panelContainerSize = self.panelContainer.update( + transition: transition, + component: AnyComponent(StorageUsagePanelContainerComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + items: panelItems) + ), + environment: { + StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + } + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() + } + + self.ignoreScrolling = true + + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + + var scrollViewBounds = self.scrollView.bounds + scrollViewBounds.size = availableSize + if wasLockedAtPanels, let panelContainerView = self.panelContainer.view { + scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight + } + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + + if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + if self.isClearing { + let clearingNode: StorageUsageClearProgressOverlayNode + var animateIn = false + if let current = self.clearingNode { + clearingNode = current + } else { + animateIn = true + clearingNode = StorageUsageClearProgressOverlayNode(presentationData: component.context.sharedContext.currentPresentationData.with { $0 }) + self.clearingNode = clearingNode + self.addSubnode(clearingNode) + self.clearingDisplayTimestamp = CFAbsoluteTimeGetCurrent() + } + + let clearingSize = CGSize(width: availableSize.width, height: availableSize.height) + clearingNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - clearingSize.width) / 2.0), y: floor((availableSize.height - clearingSize.height) / 2.0)), size: clearingSize) + clearingNode.updateLayout(size: clearingSize, transition: .immediate) + + if animateIn { + clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.15) + } + } else { + if let clearingNode = self.clearingNode { + self.clearingNode = nil + + var delay: Double = 0.0 + if let clearingDisplayTimestamp = self.clearingDisplayTimestamp { + let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp + if timeDelta < 0.12 { + delay = 0.0 + } else if timeDelta < 0.4 { + delay = 0.4 + } + } + + if delay == 0.0 { + let animationTransition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + animationTransition.setAlpha(view: clearingNode.view, alpha: 0.0, completion: { [weak clearingNode] _ in + clearingNode?.removeFromSupernode() + }) + } else { + clearingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: delay, removeOnCompletion: false, completion: { [weak clearingNode] _ in + clearingNode?.removeFromSupernode() + }) + } + } + } + + return availableSize + } + + private func reportClearedStorage(size: Int64) { + guard let component = self.component else { + return + } + guard let controller = self.controller?() else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) + } + + private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) { + guard let component = self.component else { + completion() + return + } + + self.statsDisposable = (component.context.engine.resources.collectStorageUsageStats() + |> deliverOnMainQueue).start(next: { [weak self] stats in + guard let self, let component = self.component else { + completion() + return + } + + var existingCategories = Set() + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = stats.totalStats + } + for (category, value) in contextStats.categories { + if value.size != 0 { + existingCategories.insert(StorageUsageScreenComponent.Category(category)) + } + } + + if firstTime { + self.currentStats = stats + self.existingCategories = existingCategories + } + + var peerItems: [StoragePeerListPanelComponent.Item] = [] + + if component.peer == nil { + for item in stats.peers.values.sorted(by: { lhs, rhs in + let lhsSize: Int64 = lhs.stats.categories.values.reduce(0, { + $0 + $1.size + }) + let rhsSize: Int64 = rhs.stats.categories.values.reduce(0, { + $0 + $1.size + }) + return lhsSize > rhsSize + }) { + let itemSize: Int64 = item.stats.categories.values.reduce(0, { + $0 + $1.size + }) + peerItems.append(StoragePeerListPanelComponent.Item( + peer: item.peer, + size: itemSize + )) + } + } + + if firstTime { + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate))) + } + + class RenderResult { + var messages: [MessageId: Message] = [:] + var imageItems: [StorageFileListPanelComponent.Item] = [] + var fileItems: [StorageFileListPanelComponent.Item] = [] + var musicItems: [StorageFileListPanelComponent.Item] = [] + } + + self.messagesDisposable = (component.context.engine.resources.renderStorageUsageStatsMessages(stats: contextStats, categories: [.files, .photos, .videos, .music], existingMessages: self.currentMessages) + |> deliverOn(Queue()) + |> map { messages -> RenderResult in + let result = RenderResult() + + result.messages = messages + + var mergedMedia: [MessageId: Int64] = [:] + if let categoryStats = contextStats.categories[.photos] { + mergedMedia = categoryStats.messages + } + if let categoryStats = contextStats.categories[.videos] { + for (id, value) in categoryStats.messages { + mergedMedia[id] = value + } + } + + if !mergedMedia.isEmpty { + for (id, messageSize) in mergedMedia.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if media is TelegramMediaImage { + matches = true + break + } else if let file = media as? TelegramMediaFile { + if file.isVideo { + matches = true + break + } + } + } + + if matches { + result.imageItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + if let categoryStats = contextStats.categories[.files] { + for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isSticker || file.isCustomEmoji { + } else { + matches = true + } + } + } + + if matches { + result.fileItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + if let categoryStats = contextStats.categories[.music] { + for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if media is TelegramMediaFile { + matches = true + } + } + + if matches { + result.musicItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + return result + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let component = self.component else { + completion() + return + } + + if !firstTime { + if let peer = component.peer, let controller = self.controller?() as? StorageUsageScreen, let childCompleted = controller.childCompleted { + let contextStats: StorageUsageStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + var totalSize: Int64 = 0 + for (_, value) in contextStats.categories { + totalSize += value.size + } + + if totalSize == 0 { + childCompleted({ [weak self] in + completion() + + if let self { + self.controller?()?.dismiss(animated: true) + } + }) + return + } else { + childCompleted({}) + } + } + } + + if !firstTime { + self.currentStats = stats + self.existingCategories = existingCategories + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + } + + self.currentMessages = result.messages + + self.imageItems = StorageFileListPanelComponent.Items(items: result.imageItems) + self.fileItems = StorageFileListPanelComponent.Items(items: result.fileItems) + self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems) + + if self.selectionState != nil { + if result.imageItems.isEmpty && result.fileItems.isEmpty && result.musicItems.isEmpty && peerItems.isEmpty { + self.selectionState = nil + } else { + self.selectionState = nil + } + } + + self.isClearing = false + + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + + completion() + }) + }) + } + + private func openPeer(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let controller = self.controller?() else { + return + } + + let childController = StorageUsageScreen(context: component.context, makeStorageUsageExceptionsScreen: component.makeStorageUsageExceptionsScreen, peer: peer) + childController.childCompleted = { [weak self] completed in + guard let self else { + return + } + self.reloadStats(firstTime: false, completion: { + completed() + }) + } + controller.push(childController) + } + + private func requestClear(categories: Set, peers: Set, messages: Set) { + guard let component = self.component else { + return + } + let context = component.context + + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + + let clearTitle: String + if categories == self.existingCategories { + clearTitle = presentationData.strings.StorageManagement_ClearAll + } else { + clearTitle = presentationData.strings.StorageManagement_ClearSelected + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: clearTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + self?.commitClear(categories: categories, peers: peers, messages: messages) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.controller?()?.present(actionSheet, in: .window(.root)) + } + + private func commitClear(categories: Set, peers: Set, messages: Set) { + guard let component = self.component else { + return + } + + if !categories.isEmpty { + let peerId: EnginePeer.Id? = component.peer?.id + + var mappedCategories: [StorageUsageStats.CategoryKey] = [] + for category in categories { + switch category { + case .photos: + mappedCategories.append(.photos) + case .videos: + mappedCategories.append(.videos) + case .files: + mappedCategories.append(.files) + case .music: + mappedCategories.append(.music) + case .other: + break + case .stickers: + mappedCategories.append(.stickers) + case .avatars: + mappedCategories.append(.avatars) + case .misc: + mappedCategories.append(.misc) + } + } + + self.isClearing = true + self.state?.updated(transition: .immediate) + + let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let component = self.component, let currentStats = self.currentStats else { + return + } + var totalSize: Int64 = 0 + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for category in categories { + let mappedCategory: StorageUsageStats.CategoryKey + switch category { + case .photos: + mappedCategory = .photos + case .videos: + mappedCategory = .videos + case .files: + mappedCategory = .files + case .music: + mappedCategory = .music + case .other: + continue + case .stickers: + mappedCategory = .stickers + case .avatars: + mappedCategory = .avatars + case .misc: + mappedCategory = .misc + } + + if let value = contextStats.categories[mappedCategory] { + totalSize += value.size + } + } + + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) + }) + } else if !peers.isEmpty { + self.isClearing = true + self.state?.updated(transition: .immediate) + + var totalSize: Int64 = 0 + if let peerItems = self.peerItems { + for item in peerItems.items { + if peers.contains(item.peer.id) { + totalSize += item.size + } + } + } + + let _ = (component.context.engine.resources.clearStorage(peerIds: peers) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) + }) + } else if !messages.isEmpty { + var messageItems: [Message] = [] + var totalSize: Int64 = 0 + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = self.currentStats?.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = self.currentStats?.totalStats ?? StorageUsageStats(categories: [:]) + } + + for id in messages { + if let message = self.currentMessages[id] { + messageItems.append(message) + + for (_, value) in contextStats.categories { + if let size = value.messages[id] { + totalSize += size + } + } + } + } + + self.isClearing = true + self.state?.updated(transition: .immediate) + + let _ = (component.context.engine.resources.clearStorage(messages: messageItems) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) + }) + } + } + + private func openKeepMediaCategory(mappedCategory: CacheStorageSettings.PeerStorageCategory, sourceView: StoragePeerTypeItemComponent.View) { + guard let component = self.component else { + return + } + let context = component.context + let makeStorageUsageExceptionsScreen = component.makeStorageUsageExceptionsScreen + + let pushControllerImpl: ((ViewController) -> Void)? = { [weak self] c in + guard let self else { + return + } + self.controller?()?.push(c) + } + let presentInGlobalOverlay: ((ViewController) -> Void)? = { [weak self] c in + guard let self else { + return + } + self.controller?()?.presentInGlobalOverlay(c, with: nil) + } + + let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) + let accountSpecificSettings: Signal = context.account.postbox.combinedView(keys: [viewKey]) + |> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + + return cacheSettings + } + |> distinctUntilChanged + + let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings + |> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in + return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in + var result: [(peer: FoundPeer, value: Int32)] = [] + + for item in accountSpecificSettings.peerStorageTimeoutExceptions { + let peerId = item.key + let value = item.value + + guard let peer = transaction.getPeer(peerId) else { + continue + } + let peerCategory: CacheStorageSettings.PeerStorageCategory + var subscriberCount: Int32? + if peer is TelegramUser { + peerCategory = .privateChats + } else if peer is TelegramGroup { + peerCategory = .groups + + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init) + } + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + peerCategory = .groups + } else { + peerCategory = .channels + } + if peerCategory == mappedCategory { + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + subscriberCount = cachedData.participantsSummary.memberCount + } + } + } else { + continue + } + + if peerCategory != mappedCategory { + continue + } + + result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) + } + + return result.sorted(by: { lhs, rhs in + if lhs.value != rhs.value { + return lhs.value < rhs.value + } + return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle + }) + } + } + + let cacheSettings = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> map { sharedData -> CacheStorageSettings in + let cacheSettings: CacheStorageSettings + if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = CacheStorageSettings.defaultSettings + } + + return cacheSettings + } + + let _ = (combineLatest( + cacheSettings |> take(1), + peerExceptions |> take(1) + ) + |> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in + let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max + + let applyValue: (Int32) -> Void = { value in + let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in + var cacheSettings = cacheSettings + cacheSettings.categoryStorageTimeout[mappedCategory] = value + return cacheSettings + }).start() + } + + var subItems: [ContextMenuItem] = [] + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var presetValues: [Int32] = [ + Int32.max, + 31 * 24 * 60 * 60, + 7 * 24 * 60 * 60, + 1 * 24 * 60 * 60 + ] + if currentValue != 0 && !presetValues.contains(currentValue) { + presetValues.append(currentValue) + presetValues.sort(by: >) + } + + for value in presetValues { + let optionText: String + if value == Int32.max { + optionText = presentationData.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: presentationData.strings, value: value) + } + subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in + if currentValue == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, f in + applyValue(value) + f(.default) + }))) + } + + subItems.append(.separator) + + if peerExceptions.isEmpty { + let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException + subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in + if case .privateChats = mappedCategory { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) + } else { + return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor) + } + }, action: { _, f in + f(.default) + + if let exceptionsController = makeStorageUsageExceptionsScreen(mappedCategory) { + pushControllerImpl?(exceptionsController) + } + }))) + } else { + subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, totalCount: peerExceptions.count, action: { c, _ in + c.dismiss(completion: { + + }) + if let exceptionsController = makeStorageUsageExceptionsScreen(mappedCategory) { + pushControllerImpl?(exceptionsController) + } + }), false)) + } + + if let sourceLabelView = sourceView.labelView { + let items: Signal = .single(ContextController.Items(content: .list(subItems))) + let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView)) + + let contextController = ContextController( + account: context.account, + presentationData: presentationData, + source: source, + items: items, + gesture: nil + ) + sourceView.setHasAssociatedMenu(true) + contextController.dismissed = { [weak sourceView] in + sourceView?.setHasAssociatedMenu(false) + } + presentInGlobalOverlay?(contextController) + } + }) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class StorageUsageScreen: ViewControllerComponentContainer { + private let context: AccountContext + + fileprivate var childCompleted: ((@escaping () -> Void) -> Void)? + + public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) { + self.context = context + + super.init(context: context, component: StorageUsageScreenComponent(context: context, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen, peer: peer), navigationBarAppearance: .transparent) + + if peer != nil { + self.navigationPresentation = .modal + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + } +} + +private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + +final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem { + fileprivate let context: AccountContext + fileprivate let peers: [EnginePeer] + fileprivate let totalCount: Int + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void + + init(context: AccountContext, peers: [EnginePeer], totalCount: Int, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { + self.context = context + self.peers = peers + self.totalCount = totalCount + self.action = action + } + + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol { + private let item: MultiplePeerAvatarsContextItem + private var presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + private let avatarsNode: AnimatedAvatarSetNode + private let avatarsContext: AnimatedAvatarSetContext + + private let buttonNode: HighlightTrackingButtonNode + + private var pointerInteraction: PointerInteraction? + + init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isAccessibilityElement = false + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.highlightedBackgroundNode.alpha = 0.0 + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.textNode.maximumNumberOfLines = 1 + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isAccessibilityElement = true + self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording + + self.avatarsNode = AnimatedAvatarSetNode() + self.avatarsContext = AnimatedAvatarSetContext() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.highlightedBackgroundNode) + self.addSubnode(self.textNode) + self.addSubnode(self.avatarsNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.highlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.isUserInteractionEnabled = true + } + + deinit { + } + + override func didLoad() { + super.didLoad() + + self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.75 + } + }, willExit: { [weak self] in + if let strongSelf = self { + strongSelf.highlightedBackgroundNode.alpha = 0.0 + } + }) + } + + private var validLayout: (calculatedWidth: CGFloat, size: CGSize)? + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 14.0 + let verticalInset: CGFloat = 12.0 + + let rightTextInset: CGFloat = sideInset + 36.0 + + let calculatedWidth = min(constrainedWidth, 250.0) + + let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize) + let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.totalCount)) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + + let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) + + let combinedTextHeight = textSize.height + return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in + self.validLayout = (calculatedWidth: calculatedWidth, size: size) + let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + let avatarsContent: AnimatedAvatarSetContext.Content + + let avatarsPeers: [EnginePeer] = self.item.peers + + avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false) + + let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) + self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + } + + @objc private func buttonPressed() { + self.performAction() + } + + private var actionTemporarilyDisabled: Bool = false + + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + + func performAction() { + if self.actionTemporarilyDisabled { + return + } + self.actionTemporarilyDisabled = true + Queue.mainQueue().async { [weak self] in + self?.actionTemporarilyDisabled = false + } + + guard let controller = self.getController() else { + return + } + self.item.action(controller, { [weak self] result in + self?.actionSelected(result) + }) + } + + var isActionEnabled: Bool { + return true + } + + func setIsHighlighted(_ value: Bool) { + if value { + self.highlightedBackgroundNode.alpha = 1.0 + } else { + self.highlightedBackgroundNode.alpha = 0.0 + } + } + + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { + return self + } +} + +private class StorageUsageClearProgressOverlayNode: ASDisplayNode { + private let presentationData: PresentationData + + private let blurredView: BlurredBackgroundView + private let animationNode: AnimatedStickerNode + private let progressTextNode: ImmediateTextNode + private let descriptionTextNode: ImmediateTextNode + private let progressBackgroundNode: ASDisplayNode + private let progressForegroundNode: ASDisplayNode + + private let progressDisposable = MetaDisposable() + + private var validLayout: CGSize? + + init(presentationData: PresentationData) { + self.presentationData = presentationData + + self.blurredView = BlurredBackgroundView(color: presentationData.theme.list.plainBackgroundColor.withMultipliedAlpha(0.7), enableBlur: true) + + self.animationNode = DefaultAnimatedStickerNodeImpl() + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ClearCache"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + self.animationNode.visibility = true + + self.progressTextNode = ImmediateTextNode() + self.progressTextNode.textAlignment = .center + + self.descriptionTextNode = ImmediateTextNode() + self.descriptionTextNode.textAlignment = .center + self.descriptionTextNode.maximumNumberOfLines = 0 + + self.progressBackgroundNode = ASDisplayNode() + self.progressBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2) + self.progressBackgroundNode.cornerRadius = 3.0 + + self.progressForegroundNode = ASDisplayNode() + self.progressForegroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor + self.progressForegroundNode.cornerRadius = 3.0 + + super.init() + + self.view.addSubview(self.blurredView) + self.addSubnode(self.animationNode) + self.addSubnode(self.progressTextNode) + self.addSubnode(self.descriptionTextNode) + //self.addSubnode(self.progressBackgroundNode) + //self.addSubnode(self.progressForegroundNode) + } + + deinit { + self.progressDisposable.dispose() + } + + func setProgressSignal(_ signal: Signal) { + self.progressDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] progress in + if let strongSelf = self { + strongSelf.setProgress(progress) + } + })) + } + + private var progress: Float = 0.0 + private func setProgress(_ progress: Float) { + self.progress = progress + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .animated(duration: 0.5, curve: .linear)) + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + transition.updateFrame(view: self.blurredView, frame: CGRect(origin: CGPoint(), size: size)) + self.blurredView.update(size: size, transition: transition) + + let inset: CGFloat = 24.0 + let progressHeight: CGFloat = 6.0 + let spacing: CGFloat = 16.0 + + let imageSide = min(160.0, size.height - 30.0) + let imageSize = CGSize(width: imageSide, height: imageSide) + + let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels((size.height - imageSize.height) / 2.0) - 50.0), size: imageSize) + self.animationNode.frame = animationFrame + self.animationNode.updateLayout(size: imageSize) + + let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight) + self.progressBackgroundNode.frame = progressFrame + let progressForegroundFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: floorToScreenPixels(progressFrame.width * CGFloat(self.progress)), height: progressHeight) + if !self.progressForegroundNode.frame.origin.x.isZero { + transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true) + } else { + self.progressForegroundNode.frame = progressForegroundFrame + } + + self.descriptionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_KeepOpenedDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) + let descriptionTextSize = self.descriptionTextNode.updateLayout(CGSize(width: size.width - inset * 3.0, height: size.height)) + var descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - descriptionTextSize.width) / 2.0), y: animationFrame.maxY + 52.0), size: descriptionTextSize) + + self.progressTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_NoProgress, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + let progressTextSize = self.progressTextNode.updateLayout(size) + var progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: descriptionTextFrame.minY - spacing - progressTextSize.height), size: progressTextSize) + + let availableHeight = progressTextFrame.minY + if availableHeight < 100.0 { + let offset = availableHeight / 2.0 - spacing + descriptionTextFrame = descriptionTextFrame.offsetBy(dx: 0.0, dy: -offset) + progressTextFrame = progressTextFrame.offsetBy(dx: 0.0, dy: -offset) + self.animationNode.alpha = 0.0 + } else { + self.animationNode.alpha = 1.0 + } + + self.progressTextNode.frame = progressTextFrame + self.descriptionTextNode.frame = descriptionTextFrame + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift new file mode 100644 index 00000000000..09fd43a2fc0 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift @@ -0,0 +1,157 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode +import SolidRoundedButtonComponent + +final class StorageUsageScreenSelectionPanelComponent: Component { + let theme: PresentationTheme + let title: String + let label: String? + let isEnabled: Bool + let insets: UIEdgeInsets + let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + label: String?, + isEnabled: Bool, + insets: UIEdgeInsets, + action: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.label = label + self.isEnabled = isEnabled + self.insets = insets + self.action = action + } + + static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + class View: UIView { + private let backgroundView: BlurredBackgroundView + private let separatorLayer: SimpleLayer + private let actionButton = ComponentView() + + private var component: StorageUsageScreenSelectionPanelComponent? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.separatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + self.component = component + + let topInset: CGFloat = 8.0 + + let bottomInset: CGFloat + if component.insets.bottom == 0.0 { + bottomInset = topInset + } else { + bottomInset = component.insets.bottom + 10.0 + } + + let height: CGFloat = topInset + 50.0 + bottomInset + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: component.title, + label: component.label, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: component.theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: component.theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + isEnabled: component.isEnabled, + animationName: nil, + iconPosition: .right, + iconSpacing: 4.0, + action: { [weak self] in + guard let self else { + return + } + self.component?.action() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0) + ) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topInset), size: actionButtonSize)) + } + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 178e56efa0d..e7140a7d5f3 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -243,9 +243,10 @@ public final class TextNodeWithEntities { let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current + itemLayer.dynamicColor = item.textColor } else { let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize)) + itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) @@ -407,9 +408,10 @@ public class ImmediateTextNodeWithEntities: TextNode { let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current + itemLayer.dynamicColor = item.textColor } else { let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize)) + itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index d4cbd23f251..77a642802d9 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -18,7 +18,7 @@ private func roundUp(_ numToRound: Int, multiple: Int) -> Int { return numToRound + multiple - remainder } -public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, firstFrameOnly: Bool) { +public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter, firstFrameOnly: Bool, customColor: UIColor?) { let work: () -> Void = { guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil, unpremultiplyAlpha: false) else { return diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/Contents.json new file mode 100644 index 00000000000..96db762e397 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "mask.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/mask.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/mask.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bf429af6c9c564a2d20bdfe8942cecf30af79878 GIT binary patch literal 5801 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json new file mode 100644 index 00000000000..efa7620e6bb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_editor_check (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Done.imageset/ic_editor_check (2).pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3af47d0bd794e62a86635bbdaa06e75d1c4893f GIT binary patch literal 4104 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 PtTTAJ`njxgN@xNAonQWM literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json new file mode 100644 index 00000000000..0a2222bdd6a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeFill.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Fill.imageset/shapeFill.png new file mode 100644 index 0000000000000000000000000000000000000000..ef2f0f598a9e6edcbd8fefdece7a5b3403304648 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S903?%u>HW~marvRT2SN8&+|CE8RxfA+;|Nj5}g!=mn4D#b08YTo>*pO4b{3HVdqraz%V@L(#+v}-Kj}3U*9{Oh` z-3*m@SAS(`ys_)b-f73qGu~Uk8TwUJ(D-pdP1Y?H`=xh8js?D0^C?oVg}Gq*lUehA z{9^@4BOMVZQf4qZ0xY5z8xx!KS1r0%>{r>9Q;bDFm9 zvz^3k&ES_bz3TP)?Mwk50}lDFj$tt9^xybbLDE6pZny4Th7*q#Z~MaIaP;InG5tfj zuLU!Y>B>xZ>+*iS7A9D#+Dr+b*F7< zBapu+G4q4aG`1buftd`Zv6oH%iZ7jPanR@SrQJWa7|cGpyiu~X)-|DXSLXADCCWR_ rXqjcISp-IYD)v>ZsSFJbz0bJ%PU7;fTRSX(fzROS>gTe~DWM4foL~PU literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json new file mode 100644 index 00000000000..a80e618bcde --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeFlip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Flip.imageset/shapeFlip.png new file mode 100644 index 0000000000000000000000000000000000000000..14f33e51d6808ce46de439bb6339e84ce8454c96 GIT binary patch literal 714 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S903?%u>HW~maj{u(#SN8&+|I`4iRiAzU&6Fq! z@(X5A5HQ&PKOkYg!~F&M_3QiJA1Ig*fBr(l;uVK$7#Nr&JzX3_Dj46+2zPpFAiz?c zvc36FQ&`=@f9iKlQa0F3+9P=VJevz+Yc$KB!ZMeHBc>+~ZcSkqwzcFq(=+c0gRrpY z{R1Z&&je3)FYd`P^-t=VxZ2G1qQuTD?^v)C z1)t#R4*kIDMn?1JtJB$~4WF!>pL}D{7Xv|4H@3SOb~mlx)KtaXKmGW=)xoxyLytr2 zRV4Lp@I3TCc6@&;TlB-wyUe!rMWr{|l<%*qJf_y8Qdu1qZ|Y}TRe%5e`B%SvE?HkO zp7^C=<+0HBcf7W&+Y{!kPt|bWG z^iZ%=UV1FX?3A=;|B|T5TU@X2Uvf3ov*lGpVDdTnrPpOvXT=oHKd||k!HQz{o0)vw zQij=67R|4iZS{WMEDyGgt5k2eS1!ov0lML=u}nf(_9TUC&;5$7=-qIuydq1<+nwEIgF_jOV6XV;!Da9J1ie&y#^U+tCS*LohCDp_^o>Ngc(Pp^qU>rYDF3tVzd zxTjOHYRam@tAV$QZ(RM=TJ>>l+;LXFkkVIYNmdKI;Vst09=P{aR2}S literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/Contents.json new file mode 100644 index 00000000000..85ccbd81c94 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "expand.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/expand.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/FontArrow.imageset/expand.pdf new file mode 100644 index 0000000000000000000000000000000000000000..91d9147841ca1bf8248d3a01b928a2f33e8b9736 GIT binary patch literal 2058 zcmZuy%Wm5+5WMp%cnOdkLXqMd2m&-tQxt8{)afng!L^mdg)Oy~Qnda0&Po*hlmUYt zb9ZNFR^-|J?cJpko-xHGZ@&LvoL^n>>uXlk3;M~*UA*{M)z94v*8r!qtL-+`Y|V<> z>i2S9P2b({$;0lidCPt=C1n3_IW8{qD*va2b1{}EC#?70Mm}F}BlEVrXy-LU8Y7*u zfh#GT3_eD_Ce%fI9HBZG6Ep|rEjCCV1^x-q5M)bdtpnsdqAj0Otd?bI6hIwp={@); zWwnWfXr(=RAFD=T5g+^zctmBzUYJ3UlpYMVFlC>44bW>>8cSd_pe^|dpaE;GxiUg& ztwZEkI9+P&8=zQn1e7|KGKW&}6GfX=$r z!C6ClNEq)>A_}8qaI}l`3%x+rMPH68V-4KcLEN&2R+Iq>5Vx$f6;^4b zBzctVu)BB(f zC7j#eg?lkW-Db92RU1a`y}yMV=WVmDF8KL!aWRNeOq-_T)Nn6w@v!_?fqj2#&o*7( Ojtbhbv$MCK@BRanBd{(2 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/Contents.json new file mode 100644 index 00000000000..d46f8892353 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "grayscale.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/grayscale.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Grayscale.imageset/grayscale.png new file mode 100644 index 0000000000000000000000000000000000000000..43b56b692d377fb5c4ea279c0909c23b6d8ec089 GIT binary patch literal 787 zcmV+u1MK{XP)@Aq0Pm>Q(NI16cF7N=TI%L@Ti zkv-0&2d5T%0Rp87CWsOR1Z6r(j09nyTTsRpjx`ZPU_y3$Fl{tWof$$vfJh(=Q?%9^ zV53QFZU_h=D<(-9rvQK%2m7P0c|bz%gR)O{Ae;jbbDvO@{hYq$b97D2tMsP!x`v78 zv$FcL@VL}$uJfGb-?9%tjZNi;X_-p9y|G^@ldkUzCw)up5+Eu-*`L-~zPNuvB8uA@ z1*B$*itbyMFN7GXI!%vj1_Kz2{+dmD0D5+q=k5G4`^9Ifx@2!uPmSt7Yc4NngQnMW z4FMy0N%s@o+}1%zsbQHdb}z4MJg^CQCJ@78K_Te!&73@2`A>UItloFOPQL8XFi+Qz z;h9EYh^juA7_E~~1bqjTUnAFq`>A?yti5hu=>ZsTZy^l{%sHGG?bp*w^LC#V4p}|0 z^jghl-WLy_jL+&5dUlZ)CAie6d~}VnUY=;WM>*F3kaIzyaeWs#*JrFbftW{7-Cd)5 z-@WWUC1`%nJ?j5}#I-j7p&|467O1$9zj2~>(if3A?F6efm7h0z&K|Q@0gbYqsedFh zhHrV!eDq!E{W@o!i}X9!0A&6Q=eY?hSo~dnsEO5w=lcD;d$-%)EcAP_$3e@FO50E6>IL^jfdX$p{LRly)#fSpHD&1HvhZR{Nhy4p` z_vdF6fY_Q$ZXJJWi`D8@A&7AcfE#@Q`BSuDc5oDuN(rouC#bjuz|1h0HyjqrQ@F_T zW|p<|XDX%NWJsk}ppevr6Ex#;V%$?Alo1yxJq+@uk}FZUQfXGHF%3waR)(rE6zzqY zSdz|iJO_<51B68$rE4zItsIQi7Q{3|oForOzV1J6PF1V_t|x~w|GUudrIzMphEuC>D3T2iN5T{qj1zV>W=M|o*gQpOHw!pbdi z0Rx^?yomVymgFB3^r&*!dr(){IH}?C+*Hn4zjO4p49j@ynH@{CT(LAT)#%_9x#`P; z&oFbn_Rgpr@X$}Wtu$}yk4pqUEJ(P<&lx;=Yv_i7x!bS%j0(4Dx5g*6l_UdXn#a+X zUMZ*_TI>1XwbiQ}omcm-)4lF_zr2*$quOP9xO6NK8SAkS`YvSE3+IkcJdyZq)9&EB zT%XX&wy;J^c!}PBH9y}8J>HukI~m%gm31?>KTqT2zLO_O_svK{=LiqH#`;e7tGLzt zRqF+hS(}=c=8o%{xaF;)T$~Rz z+7^xPYH!(ezUF&X)Lr|gc*~vW{3B=QiE9r2e$`lZVE*B*)3%a(E4uX`&FZSMu4(dY zY#B7X^NVt*eY)mIx=$$28Q(k>rLOUvv{t+a-wbkG{yfI~M7FG!JUySe8Pt-$H!+(q mJdx$M&2<;;if3xMAv!ivx<6uJ#SqsoLSkI9@w1ru&OZRv5g?KP literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json new file mode 100644 index 00000000000..8543bdd0ca7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "spectrum.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/spectrum.png b/submodules/TelegramUI/Images.xcassets/Media Editor/RoundSpectrum.imageset/spectrum.png new file mode 100644 index 0000000000000000000000000000000000000000..040a227706438ddd8b813bfcede8313a44161a1e GIT binary patch literal 8976 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 JgSt!1e*kTL(dGaE literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json new file mode 100644 index 00000000000..a318c3b3bfa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeArrow.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeArrow.imageset/shapeArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..3b9bcccbee9db763190916d1102d0c6ced9544b2 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~L4Z$)t9yaZe^P*p-5VyLUjC9G zzhH)h`1kh}4Ce25C=i%%K0q$+^(3IwR8JSjkcwMxFGh16HsEmy+&Ss7`<$%*yU!jM z;raK4#cSm|CO#>ZKoQsY7deYlqAaUI%k}SQu3WMB$eW!t+bq{6oRnf5%#nA|FYxY!(fY=yC$9@t{3^snWMynubT_B-UK?3!PC{xWt~$(696v_jluu` literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json new file mode 100644 index 00000000000..f299d75d3bf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeBubble.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeBubble.imageset/shapeBubble.png new file mode 100644 index 0000000000000000000000000000000000000000..fe5c8de653fe8cce094a6b3099af6c33cbf81969 GIT binary patch literal 475 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`ol8NS0G|+7_X3~))BvnipMC(%6fX(# z3uX|QAE59)!D0XX2?h-d3i9jwH=K{Zus&jXcp(D=qqe7uV@O5Z+iSPG4lBqUc^LRV z%cNbd{Q2MeCBAw(H!qh&MsKKT=4~#sDB83}h%ZPy%zAs- zChmC_YGp=?KCr3QJv+5P#DmR|^`=~)wSEd~=#fcZj#^Bi3Sdw(no+ic0yFXo; zyqt_gW~;8W6HIE#^7$9PqP+@?atvGB9#f(N1(OslMWW9{x6XVLk~8n5 zY^a})4Hxs|rMrw5m_(^5>Yb}x|3TqjXK`NVH1=}iSn?R=jV{AGx-`Wu|7<+L;QH+x;ZCtU9{`-^=p-{C%El>ZuEJS96%O`NjBV@Xjq> zZn&i|Bl&awC5-}6w=G@K?iYNRey;t%x%h#2$zA?VMSELjjz=eeAgTe~DWM4f D?yBv^ literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json new file mode 100644 index 00000000000..5331f04cdcd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeEllipse.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeEllipse.imageset/shapeEllipse.png new file mode 100644 index 0000000000000000000000000000000000000000..c62655e6d445a6f91517a2d8f43b51fea6f79b66 GIT binary patch literal 556 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~O@L2`t9yaZe{w@W4&y!dbIa&n8ORB5? zr|)&EGLl=i{rz(}bH0d$^>uvzS^9ckaC*)>ygD>#b$`ZV?Jc|ZT@|0u_)5a}+1jAs zUEXcaUw%?8m=?Wqrt(B4?R&DVX<_f2 zoOJI8e+s3Hc5Pnjpue$+i&0F1ul)e$mKW@b3^Ecry>qo94hG#iSkBR`r20U_Xmu8I z*hLpnp9L$bnhYMu*E_xV+kJP%D<4so0`vD;Aq(~FzS$=l|NVS^ynw~`zsRi1r;4P2(Z=BE>gTe~DWM4f D`hgUM literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json new file mode 100644 index 00000000000..76d78cbfdf8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeRectangle.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeRectangle.imageset/shapeRectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..052b65140cb4451978d741f9d52b18154e44bb35 GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~L4Z$)t9yaZe^P*p-5VyLUV)Mz zzhH)h_4nWZKd*3Mf`Pz-4fFf+?^~t<!<>c+rt|bTdvxD+kg8t z+ZBp3fTJUj#_^2-H@50v`qIz#681HHg7a^G&S{?9Q=JjX6Y#xU3K68bM+pG zO=jM)>pF{wp=LwbdchS*L5(-|Ug{9+ZMS)%$X|KFS?<$?`EA$ub!*N4UMlFZODf_! zp0Qx3?&lwp=aW76EO@<7z}u^OUft$Pe`YU@7I=O~y{@5|&rm~~cN@@|44$rjF6*2U FngHSbl%xOv literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json new file mode 100644 index 00000000000..14f839bcbdb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shapeStar.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ShapeStar.imageset/shapeStar.png new file mode 100644 index 0000000000000000000000000000000000000000..c182350cd1bef31dec20d8c0d177cb327b3dfeee GIT binary patch literal 626 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Yk*IPt9yaZe~Ll)6_f2i1I0>$ z{DK)03L5Ue7ufHhFu~yb{Q7_e{TJ5fZ^-{EGnavZ@tCKJV@O5Z+i7?EnjCoC#AnF2 z$&1FG|84(D?bg``%O+|*zS>s&xRs?!WA6I+|C5{dy}hK);>WPOs_{tkzNnlBj^Vjx z9}m`8;?eT_RAeKmjGdVfxkEqPw~Tygfv#H4=yzl_g*1W344O6wkNIw5h`MX&tz z%`VSE<5{)U4ENXF&NQ;NUn|z4dgHF!f%WV&YHZlMZ!_Gv9<^3Rw(_#3Gn?WWhM37` z|{l6$P)pq7Xp+mR1C)eJ$v#|KVc;TTlGwghT zE<HW~ma*8raoSN8&+{}hApD<<2428xvg z`2{lwBs4h8f4^Vh{)F=j4C)KkZwR;$pPy%$WX!<8c-_;*F{C2y?G4XOw+(n00t*CK zGCocD^0$9htJumv0fLgVSFbN`n8q_}jnGr;T`^5FPaeOSJ42K~wmsvMW@7O_lVb|< z3EiK}@4xwL9#&@Oqq#J5&dtgx+t=_l`2GCOqH~Wip(g#Ay{NnCqLVc2)-|1GbF zb9EX1R4o5vq$;%~w2nFPj}$>J<>MQk-8s_vdPBfzSEs-M<)o_Hk_w zTxsBD`E03c%-pEs?nxTCF2S!}23j+3Kcvri?BN-X2Jf4~Ct8me@)w;GYgjIF)TxT` zUC?tro4(6oH-7GX%CO46`t-Si1$7Dy=CXMY3nU-!&YUd795jE6hKw1|pznArIu{>~1dGB_~ zHO^5vY!}Yn^PhfQsN}?afA12859gO3RV#b@vUpp?^e^vrR?IK?J#FWu@Lyip*-@)1 zPp9uQTPlCR_tlYYr{{*$uS~jE<|)Krv+nsrUq+u-zFQ=g9Cg|oK0)o|%Eg)HW~ma`v9L1SN8&+{}ceP-dPerqeM!A z{DK+oPgo$3py06oeZzc%g8qQ>8`d9K@YCc70|TSBr;B4qMcmt~2eS?v2(&z$s-3BN z+;3Ldz5kKiMmmcW=Sj?S`hKMK|2wYW*4Nx= z;_Fy0x2{T9bm+^mC++{$A8n6!HQMif(_qe_&%KJ;ZEo*BioKLDs=nfylI(AN(=dd` z)8pA8+2%{t5fgniKK+RdGFTqF=6J{xRi3j|5rUVtto|w?vL|!?wa1q#+ifHH%LM0E zpK+aHn)N!-wXBL?`_kXV+vMIT<@4e2!HO+Fzt?Z)F%G%9;lXxburheM`njxg HN@xNAjvMjk literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json new file mode 100644 index 00000000000..cfb66f3a395 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "filled.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png b/submodules/TelegramUI/Images.xcassets/Media Editor/TextFilled.imageset/filled.png new file mode 100644 index 0000000000000000000000000000000000000000..714e1b38de6e7ec11c99d7aceaf22887bc426fc1 GIT binary patch literal 859 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S903?%u>HW~n_iU6MwSN8&+{}ccL0fG1L-|yeQ zA84L~gM)&C!u!_Vg35`3l=Q6aN)w^ z-ZfT0Jt8GRe!&b4^UoI~DBOR)e?ox4f_edm_4yn06PSK6Ffi45x;TbZ#BII#vTU}2 z0Bb^`m}iCWqSmX~xAy&)zLsaf9M8C(N39)o!c(eCGQpy=4~HbDL>J zlhtp_9)4Of;eeuYipgpD$q{RL{O1;NyHQslm_}FPolBqi-?K-8` zS#9e=-d}$AL^FjZ7(04%&v0DVq5Ru+#fiy>7Ohn7OBCZRpVYqK@$@~xjzv|M0wt`f zE(tNb`YD{4!5BJOJ^6OPmuadQ2h37${)sZUX(?ou`cXe!XYOX(Np|8#jjEgH?%np{ z@}n30I(Hdr5Av1uEY@CN+V|{){_OzT~^LY?60NYQI!}a&im*YxYKJ<%*1q9r;IuE?#iIe`02v*@vxH z%op);#yZPiIHArN>m+}{!_#s5h2sk*9ul|^{v!FtiKz|i7Vw8CSKM-v|8JhO_vN$W zN~T4dH@DV0DBqgCZejn@UV}UO)AiV2zG#=fD|Y$fvF>%;?(5>doOPXUR}yWJZ1dun zZ~V1O23My3oz}Mw$T)bme8G_(GvN-qH&-6anQJnCTJO7GGxjVxd+L9U=@-MRC)B4M zGtWxga{a&tBlYR+6LzwY{a!S`b3qT;v9PJJzye%{C9YTxDW z;kSRCH<@cKzvT9>pxLS7ik>Hq|JM=O%Uym&=tG0Xn~m2rBd+!wRZRIeDRI-w^V$7P zanBy|&wTm3zuo@A^8J%JrB#&f=^lRb@`>$=gv+NUbnM~vaQYV{FsU#;GA&1IrMOEG&mHW~n__5hy{SN8&+|APSdih76uoupF| zSAjF^v(!c*a8l58_KZ5Cjc+OOObsii5yCCgcSlbKguoS%iq{r|sj0;lU_!w$Bn=Z34SCazzf zG9g0w=JLHqqQ3?$@RdILAaLc6B>Vdkefy1BtyH4UT79j5bCE~IW9~DNXWrWJ+QyR- z?&fT})#G+s?em!l2GwT!rInJ}XI|mWypW#6tGVr(mGA0n7Y-f}SiH(K${lci<54>a5zZAm9R(H4)n{Jk@VvUd^sDwv zseMN}{;Mbzyt1o2v8mCz+2Qe(?ea@GPT!Q~(fGRL)Qsu>o85SqPP;m5&g7Movm~c= zs)W3%IwTXHdriAvYocavRLE_i8YVrDD?*S1&wxdfBdfrP<|mHn(_$KCgLO zWgskLSN@qpoO%9Fl_wXcoe*7pOkK)pYx|1U#q3SxVZRTA%&%iOT~zSC;^B?5Ro~Yg zys)ruH&;>d+GR(+7L~us@Gvs$ee<*Y=eKp?L9cJkeG=1E*WB>+=dbxcgZBTl&iYv4 z@Z@{M>xbH}nwt5G8QQPcoy&fI>+q7l@HW~n_q5z)|SN8&+|D*#42M3@g0RaJ^ zG5h!LfB*iyf`URqLc;y~_vg=_UrOoIiiwz`$U`h7A`kTv)$; zy?Hk0GN3l0k|4iehWiTw1QZe+_P;ln&`>b{{QCMc?5`#1^K0g} zn^L4?xnaWO)h zxUIM3*&6obvW<&2Uz^>itrL28@HZ)GbU^Zh=0^@QEQ6ey61DEy7hei8Liy8 zRm^;4Vu}0@p;jZes@jbU+okN*1v9-@>R-6ifKkEXh2j5aLB;jVa};jv{6Fb%d-jv7a>YGO$JYt&lhnNM|YbPG~zw%VsX?Zt~165Di zqjyi1y}N$jn&&Z6Kb*3-pI_+L$u;vmRcC$YK|!a7icqJ^K1S1^Zdub1&yB!z#Ng@b K=d#Wzp$Py#27$}~ literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/Contents.json new file mode 100644 index 00000000000..7de7bf9c245 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow base.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/arrow base.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrow.imageset/arrow base.png new file mode 100644 index 0000000000000000000000000000000000000000..81541e1531d08ce5fea02b2a3faf493ce9d22438 GIT binary patch literal 5028 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 XpDX53E~#1n{njxyG&iWwC&v5-0C8Fh literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..995d2735df4f5f32616c3f03f55d9d0a651579e8 GIT binary patch literal 26873 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json new file mode 100644 index 00000000000..39362cd4254 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Arrow.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/Contents.json new file mode 100644 index 00000000000..47decbd43cc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow tip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/arrow tip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolArrowTip.imageset/arrow tip.png new file mode 100644 index 0000000000000000000000000000000000000000..17c67d9907deec7d177d5df3bce8f0f3dc097f3a GIT binary patch literal 2156 zcmeHH`8V6!9yPq89!jgCw5s}^YH6y~Y7J2}Mx{b&S!!xsS4+)}2E8P1X^9#-73UZZ1(v0IACCzOeTZD03&eb=;-M1@bK8!7?=VEA)QVK0u(?#GBN^~ zN~MC@&<}tqSo{0?X*3!T6bhxUuMZSJ0x&QzKqiyBySsaOdU}80KLRiS$Uz1^bI{Nf z4}OlKM>idu#T4TjSR!7iWF(<6Xm4$4Zfg8iUsqd0B34!Z`IS&vQC41BTvAwsH&6Eo;jw%) z8$_Z~eU?byoA=}Hz5ilB`XZhtOT;P_i5h%FqEMiDitj`sjEMJ6gaHPG=4Az7k9lZ% zK?8-w;_+Z6cPqri_W9XaU2{Q;FN|OVKA6k3@khFC{CBE&B9ip8i)jB{8cgfxrg%by zeXMqd(`e*$Z=T8HB?fVo#kspTYbJ>D=t^C>8s!O(yP?5bF7`x60mKZ_PvX%7k_KRzW-~`@VKfqd9>%GdXzp~Ll;y$MC0yVUXA%p0W_enDt{D)xLakL;_6tn@YDJHZMb=f%g|A!)ju zCfrPWrfr!Ue zu!B`WX$K?}a;#!MEF*wD7=x; zvgLXinYFA2?V-tRuR4mavlb z8!Yd>h1c5-Jt5&$!H@}xKB#o0RAX-j#s?W4Rnxxo4c@zB_Q zLzN_XvGa0ztS)&21A|Es%cWCcx`khmVrBI%VgG~&xeP-Zee?6rh4RFd5O?w+qpDl z;GF+>WcaoRjbP&e2R2Q-#y?4@*jIHu8 zRZDA|fgT#YohFKgt^IBBZC`LmWNKBE zjWfjiy}hzEMBBnq0u^@jRPMk{y%f?F@`|aY<^CfYbL{t4GTB%7P7v2yo=Wm+UqT8= z)j?w=Z?uO{wO?>C`kLNHcH&~RuQH7Cca-vdDZ;g12XifS8d?G~&kFGN`@r^f8MviPNPPenhjLByW8wr)Hm z!WWMCHP-jCvXoodWRuqK8TUIBQ(KMn@R0|?@2S}R;|0UTSZN~3$(zuwt8+x<`RB^* zU}%AZ`Ouj&a%s5cgnM=nsOvx&X5RwdC>QP;NW#wF%Fkvq`SV6jUZ{`1Lx>~zPm@YE zRziHxbM>0T5}^bp4QnC@5I0omr|ic=U20&99ed)9GmjUrFHS#X980}8!B;d)Y}r(& z%DBLC?>AAeZJ>OwoLa+ArlavwsXtp{B*iAR6Y<^?TNaXDz0Xkal(_a_crFG%Z7+`& z?6X03Ywd@d-U)2{4YDc7ExW9(x8g3iEy=9=_S)x^l&a99cCJq93A$J}zZT?tzx#^o@4ZKnZCFsjmC9zNzwiL``fXjn UAABA~`)h}twS!fOg-`sy0qrv;J^%m! literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/Contents.json new file mode 100644 index 00000000000..9c9697e169c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "blur base.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/blur base.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlur.imageset/blur base.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3d03d138e857ec0aa260303ac0824dc08b3923 GIT binary patch literal 8609 zcmV;SAzt2zP)rI5;>t zMo39EH#jvnH#IjnH#s>qHa9djHa9ppI5{~pH8wLfH8M0cGd4CeG&M3aG%+(XFflSP zGBPnTGcq(ZM@UF8F)=SNF)uJMFEKGLFfc7IFg7C@Cf@~EE*deCMYQ>D=a7~D>5@PDJm;8H8m(HDk?24Cn+i_EiO1XIB|WDkN^Mx z9CT7nQveu$zyJUL|NsC0|NsC0|NqZm-~a#r|NsC0fJnDvivR#0|4BqaRCodG!7%{< z00aOqpnqj4eA@y5000000000000000;6YZ;9$$2UB`XjFQS=K^M&Z!zzW)VIWKzkr zzjnPRAW4-rNRKJWTI-xkQI>M!Bi^9J5C4Ay7X+`?sWOYrf@554k(q`@%jsU+UpA-<3b0FJpO5leIM02IQ#{#h zH`Hhc+6oN{T)1v|*3BN1`exvSy3hHU6>;LBF+ZouOtMuZT??xVTSSfKIe?znKnpK?cNFl3H{@si4U zh~=~icZXL^s{|PN25w{0_jTsRDJ9GE94iZ|?h$T!2aruPiSa(G%%D`1$Q()2JPrqU z)oc4Rm6&u|P^WRT2x+NtS0Q9))~Fy?YotaOT7N8{j>Bzvm1V zE>#r*Y)0(#IjM%5mR))P{SEvFaD(7cQYs~vOF8=wUo~2fcBusU8~6cm$M@nSolm9OY&W~@X0uXDiQ+hNT^j+7;6N9H zK2+BuR?vuv#VfBi$V#tD1n&ocE&KA4r9CzCjXtbu&PCyIb zp68z3J}&osrh_Fmp}KRhtM%rz+id}3OhxGR*Y&5KA9aMS7twd-kKRW%qL3^)?MUqao5@4?&@!Sq1_If(nz*GfWL`(%o?9p zNoi6N3sVvSW(W)p0YnibqoTWew{8XTIv}X~{XfDz|I>#vvFZ@s^ zv6FWWkNe?pFdX(mEGn!itV>$YUb$4Q(h|aNsZ^|v%0928dh57N3n{n>!(kY7n-w>| z{YKiBRII-K?JRsBTJYhaKdKh1qzaAA8B4fE8`deo{Rnneg;N$xi35RaNbE7pDs=h} zg8f5+v3GR4=fQ1wpDb}|gWxg46NWi)3os@2%4E5&Gj`nZ6!rkuu~(h}?%jtsR^n6= zI8>O2+6P=zxK!e?K0K`Pc1gQs5QIG%>NAsA+_>=iw!*{S99*P9Ce;~}U2+Ak)Bt0D zm!WP|+_XuYiVJTNTyPb}9Ctfy16MdPU< zTmtL>9=nc#z3ItxF;`rvRbh|efelv<1tv8&&QWH-BcI_0U?lFDp>_bjWj7wYmD>1T z$a$@PFM=-!Ztw;|a8+T-Y}zb=I|OTDhK;)w3x2&xWu5bz58?0;@U13B^QW-mC_Ey# zBJdNzJx0>(PFhkB$#-4XbvYv8vg@Vi*>+6RL70S_K^A>))_n7Z8ZbOTuo2VG4J zcr7lxvAtJrUJNIX6RWHl9#yNfR~uM|+6SydEyV?Ra}$B%!dn~f1*XUbzL50c!`uqY!>$WY=2#K~rYVjMr!dFe-sDQw6X5o-!c^8JVFi}N(NHVQ zwe@X9`0*N(Oo?@p>jI;!@dKEUhHK4n=jRgdD*Svs)oKPV z0d}Wwd74;Y8l#^|JOpf?Pjv%V?ZmW>32vaxoIRb=E5n-j&RS2nTflk}nAZ~E5fU@3 ziM6Ve&heGNeOXUPtT6KGP*Wq&wBeN#e-!xX{_&3BfvhJ3A8BG4Ykqq2qywR$?($GS z=mwGyzGfIZNOu-4Gc1`c*y=#uCmd>qA76w~)!4jbs682KO-w`WCoZlhg|)*aSoTbk zOXzL|M(b0jIR+e<#6A)y80t~;X{d0tkQH_b)^)6p677q`FcJ%VAHnvXIjSP>>`?m# zZsT#0;9I~6hk8D-!Xpnb)2_mrxIcx*>4fW;VSDSU*Eq3W(yAV4;?O!5%W^z% zVwOGocmmw=on%|4HPoSH67}k{zWqhJX6(I&)}mPU>$00x^P#`oxmCP z%$sw9b4gC8-z2dfPr89%s2SGY$0ZnJEqf;5fZ>E2h&9v-*R0-$U52rB0dAjlIdO;J zU%8wjSRUL!M+>>ha6o_OMZJSX_PL-4O3>MAGBabg_E zMNNF%HgLc5vx%=3CT4bQ5?4L@v5nwgQ(X4*Vwqvov_8Z{Fqbta#urWr`fBW4R^rYK zTxHnFab~n#fYAyl+@_&E*xpz@@xCUWe^4#UP(#k?qd70+mf1mqZl~cn2Zz6)-0@)zvW9nSk33FL$uoP-Dxq&*L7!T?;NUoD-;NLW{!f0;=E)h&&0V6X%#tr@kcqndLMY@Emsm>rlH4 zU*;}}<^@$Q1J<%;IN@Ghj$k{n?{Q5FjLdQ};_Cq}Yrv<8hFTl?YlY?6Nv=`d63y@> z!HheuJWT-RvhFhcdDRWXerOvS_(EYxOmdcCoD|WWHJ5e&ED8M4eoU3y7u+|=#c?%) z`5(YleSQMmJv*(JvxTH{Y#z+z8z6?!aRH`^B9VSM^O(mP>*>hAwA&Dzc&L%sg6VJq zJOb=M%^KR7!qdkXh1XYw>2(mV)fd>lqxS%h8NSSbHt&DPk*x4>-9xj7w*z9sxHjI& z_{YKh{H;&LM`C&>gcD~jvxL)%5?H_WIy*aV`0m~geG9X>6&uFJRmfF3!4R;0Zw$Ch zaL&N|!*HpH>%Aj^yWMv4u$Vo-ml9jMap7GGykD(f+$e1J4}rZYoQ>eI%dmW>3b@le z^4yaG$(ye~#Dupt@WRjWQ#hI1KXB)RW$e&e$|!6m8ND@N{`#8MQ?ur|`vvOhx8lO7 z_vxHp@7(kzce;!P=5AhKa!>FM3=Z@+uvT_+>Hsp$rmf9-EcU(zSGsD%h;pD zfO8a<5-Ti;Yk+Y~JOuRDo*wzxoj3GS)yG)y`!8PeEEo=OMA(gwUX$aUx)b_*0i2~- zX4vy1_y!L$_-6AUKD?bhJPyLiBk$A$tK$M(L{SU8Pd_x|fL#O2!ALANywh5VUL)Iz z4OdQs;bfwRVh}Yx{ycn=F>sY(S;z#3^e11`OZo3H;Bob^(eC~JCtmaUkNH?H-9+yg z{b7Cn#7AF5v0g*@*B{qE`t))U?RVqLh43ZAcQ(wf!f<~*jKmi|B(b{lu(~S$&tSL< zU`-4ea3tGsX}lCZBlxeEVR@TtQDVpo6CW;xT}`|ghOr!;PuwQhA1#6L+VAd0p?K5@ zY5#b5L_xghkFDU_Xz&5_RsLz*7`xU-VFzU?Q00Y zWx#YK5}Y^i-aZm%8P-$c>>&sxF~M{^eR}oJ@SA*@;K>B=vklXivp9RgaGqgKOfYSl zdP?X}H;a5c0Va7N{Qvx-Gv72#c#5NjOQsm`CBZzw3h$p7*aOUob*MdpQ)qr(Ernlw z{P2I@Gkz-&SoVF*AkfCuzsJR!)bV> zu)Y%^wM8&ZaeiN6O{{R|EP^w1AakSnYAL+-dSja!%$tEZ6o(PqI1>14mf*cSCr(GO zqeC6Rw^R5v-uPVxe_NwY₪hNHdS3z7!a0dRo_bXI@}Fkh_w&xI%$cR10TrrBoR zGkqS_mW=b~mtalY3%kb^z$tz+;@hjmFg?Azq5Xs4JGq6Jy>~HiHiFYrI5VFZLk-yB z)_NC_ib@~9t^Jqwr(MOQ`8wMYE29{Z_TlGd-puYU-Jy}Brk^F0UkxL-a<4Z zI5UMmrwf|cf}?eeIgSH=V%Td2gKN3BwcaB00B0$&nPP=Gac;i2df}6S-!ikuxr6u)nDVl9~FKCY~57X6wb`R zSWtk`g6JG895XTCU4?byiW1v!ffEai=Qf3Bw@f5n0Ulfdwy!_1j2T9|#qhxloPDvl zP}M69_23%t#DXgVp8!rXyg2a#CtexGI=0^cu@fu&cX|rvY?u?T3YTe)7r^eA;4gsx zCb&Shf$?zezObC0R)q%Np%<-kb`Sde*CUHC21_C&`A`djNZq+8SfeQlDvxGg= zNfRr4GY4}^&2#)&VXbO~^^jNzK7XOOqBCNDAF!49L=)R{93S3Q`1uP}3QJ;z(|{Mu z@oIZD;AoD|(POq6((2^^TZz+>IJcnn`MMf>Z5>XW7_cTrap5D0GcV3@qUAK(&Z7^| z5Yy^Y_2=w2Poanx3qo)5Lou~fDFm>RAOpARfp6BjYm0^3#nU)IiOH?HIc zqVN6(?^<)=>sgPJNH2ac;@JrTA*%%QY*@(H0u<^0KOtEW4b_|_7_(YmK=f@kSu7TL z)v)!c)wxu4D*@z*{`e4v<-|#>KJ{XCWT&GP5~Hj5Kf~jWC1mo%k|b`M?>L}OoH(3I zy*dI&6}jB(^gZrash7_Zxt>nE4a2x&yyI^T=X%agNy|w>=0&_?NsIr)iNo-|>(vr6 zSiR=`H^E)OZ;DNoHFsvt5=ZZ3wAyc1I2l^b3@to*f@8ymWs}o%9z2i7BlEkQ; zmI3xg?F2?3yt-wW@0fs|xH>=aeIMSwirVR{Ox^zx(4&N*VH@oIM{%{nTg^@t0%?}1 z;#?9y;MFMFc1e;DAkYn&;?=(!R<9n0ixVSBbWZ$l7Uo2mVi=gqF4~wRu7_cvA_(?U z%ynnfPG^|wyi9#7k!##BYbTdd9JpHU7`QrOwIGnyeNF@dLvAB2O$7qm;anqftrEF} ziZHwyWonWZ^*N>0;;8}lz;nf@Bmn(87UsEOlYBWUBe0Krj61FoNEkk}iE`FXfIeHW zJ~*-D;})~0hVR3xamO{68HP`B$IT>h)DmX@T$qPf<2i|8cy+_MNWOd#ARy$+aIVLi z137fkV$@E`2qcjUCr)s6$1qOJ`dFN6K?#$5Y76Y)jt3Bk`j|M1x zb&({b8FH9Oi#JJH+z!LIV*~6VEz)qDn1+`U?44$w6VoE#Z4ol1as9e!_^U_+;MGAX z@{WPI3_vfq`u>bydyf;d3qu`91Kt8>0H25qAvM z=b2!e<1$&oXaG@0;Jt**0&`i-5C8!(^hvBHZHzLtXIQkxXYlqH>85HvUh_|VFk^Mq z{yC?c)+|ZfcB;jiy=6C5olC^~klc8Yi^z6c*gaOKg$jIS9gy$0I zBLrK~TV#Fw+6o}@oUX#F8My@BGCLh+)J=h_!R(RCT<3Klgg(J@0dqm?Q&bYoG8JI& zvk1xJj#;$v9UHMaS49wkFn};0L!8hwJc>32dnjR$B!Iad8X;2+vsJInkZsa2M?Sv0 z?Fd3eiK=%j&#BgPF>*Bvb7=OUQk3UJ=lY~c!gx+<_y%{3d`w1w8hZ1NQ9D6^!goxS zqT|&OtBI@gJEq~jhF+Wtu^Jr6!V@D_+l4t>eC)!o;e|U!c5);)00LlrhW2({*IC2z zoMdn0XVnOhq0bfNodBYDEbx}^7<~21HZ?^^CS2x~qD-wM);ultbs%43qFxN=Ng^*{ zTn&BPG14M?5>@+0p-}-hgiNHx%$7&c_pG~}D$}#>tEvsTrA}|*_Z3vkw40n2qLO00}4uxb)i$zoO zU)B(g@LWowipc1V@LaVV<8`}#3_y=}9EKN!Oi{kL;(gvR!Jcv;elr9`+Yurou(!hc zEVyGS+Uz?H1VW|#f@xhul=BK7%Dp4V2FEN)xHyu@+ptw4mtC*8OthP`l-?3-} z*zmG(buJc!o=b^xR1#|*GFvAGrC1n7KE@`aQ**Qoe09hpwJ_flvZ`sB^O*!z!Cpy4 z@KZZf1PL~I#}ySZU$iC47u+$@VlrBGGCnmTmwsv>5C8!PP$uS`zGTW5P!X88=KQhS zcLE0jzPi3+TV%u&zn9+PwDz`WT*JApG#@91t(i6&rm!26A#|>lhW*j9T!50kV+>mT zO;3DjlBjHk0CR=m4V}x^S4VxUA|tj2C^DLFhLDPo6QeK(QH~P>-gf#<6okULFc|`{ z_m45I&@hY{bLcWFMLDz6L!b2lye0Vp)~9f;2Cl|)LUsxgH45|k#HU`!tc7ISOHda&HasG9lHYN}t2O>(;GCO&gi<2QmtG;Ml-#I{lO-+jTSs$mP-o~zo z^Y#x;9L^O7Mq0Gh#`2C4GGUlK33l2-WMr$2b9p44i}FZwoc~oA<{kmk#=C`;MB3Cm zw3)rLPrVoCVwrlO&k5=#XK54{k)%2h;_8p*tpTTKlRM^#f8B`8xuly;Q^W0F=v*o5 zllLS@MKr)3akVO6-V_MrdP?hq=^vi>O$=x(6?@xQ%R#S()<;&aK+2wMkDQ~M(r)UO z8_D2Yw&Vt^k2XW3V@{%+McZxDFrE_(7pu^qz4Z!nQV}_&$dJi0)m+(-f;~vtbK2My z3+Ac_A`k)rzf6VU4Ehk>LfAbIZ`oT!l3-z;SBpkIj%rbHweAt9rE~UbQl6MA8!!}` ziUs}YpJT2>O`y*%)zIU_7VC49v$tcMKaX5?$V|iu2$|$SOr8s$8t*uHPA|M2BcUR| zS4Tcxc1#kZTBK(0vMFU}`zP;7)XP{qg3zDq)&L1Mwg&uhezt$0^^r~+#s~A!vInDR z%X<=>@G2vhSF{Ngp(oDu)#Ng*E=)S98DLMDy+Tp$C{c;IR1o@6Ph4%Cn|a4r43>4; zYJ$xM0>ssk7J1@)8SBKPCN%;a1Fi)qGt8due1X;ncZ_%ZNuA5cPBcuOYeC55x;s}M znY0GV=d|kd&wge(dd1M^!|kXR%}*U2^W3lww?C$nA+9{~JNDj^5s<~%{(wf=P5p}H zZ!igbH zz@XKV(M_Ky>~>Df`Qw6oArPQu&&y7s10k1LR1z)%VQ=y8&;JfXT6_e`B%n9d#-$*X zT&63lg5jJINEaAXBcg6ovp|`);zwb~5yMPFGmenN0s+ zRk%AffB}uHlDHtjW_Q|H!pNV*L&1hFOfgmOzhjiC`Brv@Od((TQ+xyh)qqBk7N^IK zXc+H!lW**92M|_^l+H0brL~jMs>j@--m$>jVtb_S%k1}$VVDaH6y7F){(5?oQOvX@ z>@6bK>aM$EtY+hyJJ>kW#_VKP5}5wkQ}kPph;k>YJz91KeM-6shHram@jJ2;F;~3> z)|bxZf6##FH(S|Y=(zxIMf5BA82W0!TR(shf$+qnn_hY+D%-fEjY*=`n9Ep>PUVZl zYVcfiE^Dt3bRc(78)vLfvAfAYnNls@CmXwq!W{3|VQ*!nVG=-!+0;GgChHv&`fzgs zx~a|`Lwj3q?9NEk40Amg|NenS;IBX+`uoz2WG5KzQi|+JuuP?M(eQPI%vR%?J&Akv zk1>Q3C-$7HSh!1`>rbg+p7_T7izt4xgq8o2op!%}>T%+~@Qw|`KPrK}I5+%pocP)^ zOrl(YK>SVo>BJXdc$XV)dG$}(5Ki1NaP@8kkjJNm-HG9o`SY; zd;P&k;1%G6`;VJW!4k3TPyhV>(+3S>@a_9)_8-eFGr+X~XL1}@@bLly7!jx2e{4f> zNvieY_82&R|FKmO*RH|;1m5gZ?mxCiSk6QYF2Jt{>ua^Z>GmI6+muwBE~ZDAn7$Lv zh##CE8cw(W*gi*0*uc%)3H-Z)i@g8gRjU2R))UUU#;~MK=%z3~0xaD$-E8uN`;V=c zRD%S}fCK!QFl(zC)ziNcaiaalOm%=|Y&BS0(#C|Pn+Rj@R=WA;4n=Zvi_8n5rBQ z^77@hwg|5Pe~teYSe0rOvAlW$T>i%ccDCsOzkLT-9T6z%5{|CwOYr;8h>S4HtHBQD z14d_uOzYYz08bfl+=|-WIsBCozsBqm7(LYx*jv{0XPH~Nwhk)Q@e$ARf1@M(`7I;% n&b0tTXJuJW_w*4HzM1ML_>tw6(Z>wd00000NkvXXu0mjf#F9Pz literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Blur.png new file mode 100644 index 0000000000000000000000000000000000000000..952055729ba2c42042854e06be18da1ea5f95683 GIT binary patch literal 24274 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json new file mode 100644 index 00000000000..cf5d650c80e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Blur.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/Contents.json new file mode 100644 index 00000000000..ab6ba5297ec --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "blur tip.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/blur tip.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolBlurTip.imageset/blur tip.png new file mode 100644 index 0000000000000000000000000000000000000000..99d5e38682dc9789f0ea53cda2114893dc7ee959 GIT binary patch literal 8265 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 aE8qx|+?4W>MD#`f7b_>NELHs(67)ZDLziy= literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json new file mode 100644 index 00000000000..1ff77c3d766 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eraser.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraser.imageset/eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..c9691ac63d1c651878b3f0ed384e7821443b2d40 GIT binary patch literal 10466 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 U#oQh9hZmNHs;)|vl1=#k0egcI*8l(j literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json new file mode 100644 index 00000000000..c699ad2b987 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Eraser.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolEraserShadow.imageset/Eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..30346ce6aa85a1f2be967ad8ebbf487887b2b56c GIT binary patch literal 25395 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolMarkerShadow.imageset/Brush.png new file mode 100644 index 0000000000000000000000000000000000000000..18edfc1e2d49936408cc1d5824911e5f37f9fe6a GIT binary patch literal 24358 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 z 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 L>gTe~DWM4f=>D(% literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json new file mode 100644 index 00000000000..1aaea262c99 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeon.imageset/neon.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e196b6e8fd4af7536a8666903ec9b4b28f4c3c GIT binary patch literal 8428 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json new file mode 100644 index 00000000000..60554c019c1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonShadow.imageset/Neon.png new file mode 100644 index 0000000000000000000000000000000000000000..55b44fd1770e86c8b463b4b1172338a5240f7d9b GIT binary patch literal 24298 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json new file mode 100644 index 00000000000..1aaea262c99 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "neon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolNeonTip.imageset/neon.png new file mode 100644 index 0000000000000000000000000000000000000000..44e4b6eed619fac8ba04536c90bd23636aaf51d6 GIT binary patch literal 5395 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 VMQSt#;(p=)Xgt+XsX^F8{2$pc510S| literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json new file mode 100644 index 00000000000..aa99f8a6e5c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Pen.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ToolPenShadow.imageset/Pen.png new file mode 100644 index 0000000000000000000000000000000000000000..62291a163d57487177ae3235926437f46d7fe7d6 GIT binary patch literal 23540 zcmbqa1ydYNv;`Imi@Q4nch|)sc+lYP1b1Cvg9rBj3&GvpLvVNZ-~@-@kMF&o@VaWM zd%Ak2y5{!1=UkacH5EBD6k-$@7#K7KdFjvZeLM^d940d2`{=9ffx>(D!CC%`8w`x3 z^M4oY*DGVk_eof{&vKG5RTHE~?>F$)63P-VFtu^OXHx_i82v~EX$ehl*t3A~?-t8i zw_OQevPbVg6!u-!W%FU=y79nKgNCevDsl2;Iv`9b8R5y z5q4epYxLPTq`Ok>sU&r1HEpfA#dzn{%r92c*gg+DcBjAcYTLQGRhV3q_64AICYb~w z1``N0%sdR&opoGK2rG)5$Ep8{xO2_>wL8ZrfFG-oxR5Cz$?}l8nqFZjGBD_6n_b}M z>6_bZ%FKGynRfH9|1kdTZh*eFbNdHE@kTSC&+Rb1da^GCz6d3eZa2V;V@T#o-#~XB z#ib&XW;5L6$MY}o=U*S*M(JSi5i+JY472In% z3rQB}mA0ch+VeFx-F?9=G*9m9jwWS92ZX)H$KnNw++l#LMT-t3S5)^;#_qE3^RDZzdt+y4B= z__YuS{Mzck4)WR~-N+%Gl8;pm+dP6>wb_vgflF?x&e1nw=V(+kxM5-N@_8x_dMx(Z zeO>>~(Mq}9Ipew57&phJ*nQp)lx6=rgGY*t%FYK*{hcK5h!w9uKs&EIv)r114y|Q$ zRd-}od5z1LTxB=2n6;?Zey7ixs`WlR=wEpC%O%CzW#{cb-nGG~C(%5+Fogzn)%dv- zrvgBw}s{|59@j2VqJj+HwH}wWDn+!*=cD(JTt!L z8iFY0)i}|JT9Y4 zgcE`*4hn!T_JsU*Xx|s3{qN(q_8@MHi))js`gKVymgFcrcOg+4>0e<*tD~Ejfg?E! zol{~SB#)h&Pn}Oe_g+C>=Vg7r@T$92Lxumg!)$XOR(=`mRK8x1Wxxf9p3uF5sd-EZCk$t6!ZDr!(@ixGy8Iu-^+Ag0ys`3v7AHpF-;w(G%%zW6H z?=o{Hb(bIruWm6%+q=1V&(pMFM>Pz7(9wc2G<9<0=G#esL6Y)pBj)S3AK!Ru{t zZ2iM%%z{j>%l59Pr0S-C$q&?E?L2z9XbY8>jiG zZarSLseBZ%=wW)|L;e)boIkfTKn@~-$ojGp&3g$y)P*+ASr+krMJ&|V{Sjlsn{%$< zzt8KdRO5I09MXN+(U){e*$6R+I1Sk{`+HjBXlJ#9I7K*#`=do`d-w>YG2q!u3R|Co^IO#&3^{=jQf%b)96IjcPVBXe@lg^XAh+a!9u-Lh5VBU0D|E z$_^CB7;JV9#&K1%uiH@#12kArVgnwR19M(cs|EZ_Ws<`-^~V>Z%%&~J1+hR_si&3; z6T{+{q*EyWuG>b$WC813vTu5SD4~R^o(;;hN1lVZcr479mUEW%sjWPV(Id0=Z!KT9 zw^7D5Vr}ht)y5OPkl+X@(=m?DNiW9bAT7Y7A063TOx^tzX50VuZXP%NZ$$o(q_K81 zLtrG*`$%S=mpWC*Jeu`tNk9A?ERBt&qX@k@LK6Q6rl>Yj9rvruNjvC_57I*znlxtL zx_W&&KVOKBbW6h!f1c`k7LmxmFNUiCA7U1JitTR#tMF8TQF z*@&_5i5y2K%$p908AF)|v$-bO$Lk4c1xfZs*_NW}ww`>u-oYN&N&jM%m3h<=yIhgf zg-?n?x$u3`;F@JsC+S{q@#p+_&`S{la;}7FV}M$1ZK%>G5iZ71$CJ<^7*V7)Qt?*H zK}i7JQ*ja7sR2oc56<5wOV9YT^#^)>t5R9{T}exACL@v?;LQx$cn7zH!ge0gx3|YB zF^qohHmrM5c7ItXps*TuHgFt0fl~?ASJq;&^Q(SmnbdF)Z-$qFT16H-|q3y zan)PkX+IZ_>}(b}Uh_cGsq%ee*jHPb(^}z^Dxo z*D))5v*FnS|G5-W^?e{fIi^?(@Bu8rY8ZPKUFA34^)@U1GMl}MPw7uDzS)l+66;wK zE@n@uT$XApZ;WO+diw*R)b$H%Eo>_Tg7{NdiM@K?%?6v~6 zJoag0IAH!Uch!CK8fCZDj*_*hIl8l!9x8B!&u3uGR6qR(<`TQ^yj<-c<3I4q!J!bZ z`?rj94y|B}QXlIXABl5(Hn`A3;95byvcq`Ge(G8ZYiofLMZ<^IO| zEjhzkT>f#%(H}dVIY>PZIQI)AEyN-+>XML;{9Osi?fhyk;Lv4_A`;AnOPLAxd=AFE zJpXPRU4L{DWy6HwhAO}I?^~4$Tm-SS{|!m^C5cm4$r&*EOe|r+2D_Z6fF1gKRq2%x za2FHHFm43;%}`-O`EvCvh#!9`hB74fbXrdqHR#O7(}P6ee-YUU+VjkvM`t?GUa-GC z+Bn#ZOQkpAxq@uNmz-Mv*xwvSvs)5B!QY>h2-XI}Udz%Llpoap$j)ZH|GWNl+WmUc z9campY5cUZ__wl~o$KaqJDeg%BG|Lt(C6*tzR?R+;MZo;ii2h2wPrdCnQG80HczLq zZR#S`{kmw)(tzFb0pD>Do4y`}MdE@QIn{|=0f~-L2WOT*3_W6U5yC#)qtrS9#;^UB zl*imrZFpSX+P2zIGuu%&skg+4VaFXa_g(SSCH{(7u|*ha$N}HU`^%aZql_r}z16r1 z&I|Hb(L%$Q({I>+5>Gdt2Q(sWrKa6)_&FinlX>>NJx{)!=AUhB&kIxd(u(K|slCdpj z)j4xZVP#o3QPQLKaGrfJR&9sc>gJWan5Cke;rPiV5ioO?`ZBitDaWMD8(xq97|*g+ z5R7`6yUIe@?w|nvQWU&wv7dr1kk+~at%YLMC_wiWM(EeX7pT-YY&X9w?y83&F<&By zhM`HbCUaj?gR&(3NYsMA$y+9miUQyIFyBo7+Pv)4PvP|kU97b!`&$7_*Hsh?G@tLK zV(el84{q}I{hK=`nFCE=>25OCc!5uB6`%-gpX5zh*V>%>qvG7Q9vd;+7pDOPy}-xW z+=VAT!oYWk?nXqLP9%#o?s>zfg&hbdfM%tO8!Sda6o3A-0^-OmJ zXQcs*`;jBH+RU0ppn55gCT?#d*@*-0g<6QCs!fgT1KJ0)0x3PNa?vn`af58)L%74G z6J=ESPr4+JMhd1pca!q4{T;sjz_kH)={ay9CmwaZM3^-^dxdlSC$)#Z>*JJ~M{_09 z=R-MhS5?Ffg1^;o^Hqz+76?0*m^$R}ny=|?*G(8Y|ICGWWV;C1m$O%w$VEv?$^$B- zHtih|U&je5E!p(2)oxIJ^VqY<6`sV@uKm?0`na&w@vrqyP!(MTQb+ZZaa8ipPP!1>@K`Pty(P(K* zzmP0Bygpf{bEr%HO~)4L;IELIs>hv7(i?RoY)2LrAi^h-YTdznXF^Ang=AKg{P$JV zs-$v4@>f%0X-yR46IwOfw-`dun4*3T)pIuXBG7N2Rg%ndMdEwxC22DnI3yBw9GPcc zoOendFCvW<2z%_ppf_r=Fg91Cxnc^~g0x<0c6OxfF&b)lrVFJz#Gs>|Us=H6kzsiK zU2ElhJvbR?GxTYMS&5@YRc};qHqh~Zz8ri;Po1-{_$dij6VisV10MtF{=G*u-uRDJC-Z>3y!&Co^?kSA>hgkOBVr5KeHZkelo% zbhvOSIV!l*Elc#fBVA;KpVaGogY|H&{~GGlYfc#-g&u>zN$D=Sl(4P8`IwrPZ4%CdtKvx&EdkP13w`05Lg`>1+>>Ot@ zbak&sd;W3K0;H(CS_M?pc+fRo%w7MZczEYdPF>JjI8tSJAwc(2I7OiDYJzt1l;rqk zq>%uA2JF8at|07&hv-A1_W6XgXTTqfKlfY5_wAc6Rhu7N!H+_kCzTtKqCQvf{Yom7 zGL7P{?<%u(-i0#b>go!&!??y56c;m8q1*kjdwy=f1}&UFWF}7RKCz_pL)dW_cd&pa z@Ori?S^$@oXeKS$^WXjBjFah6cNiV1@1|28c&V?$jv`Hn7wUqJcvwYP26?dUO|^bz zbw_c?(sVpD1O*{=NuTMWWnIlrNm{LPrlzU46rjftzX^Ur=^w=hMAeEEQrX7(B`#c? z%d7-E`{B^6oLb)ZwLnsuUVVP`nf8H8SZ=n#S}5)2sFeEtkuNlY1wTtb9S#Xn37+xpcJ6>E(O2jdO6*`o&|cmd zb1}P;thYVJGv|M2*FwOMo9TH*Jvx z?F{q4YDU9p-3MM8rg)jAAZvuJgr0ma;emOjd>$|9lBO$C>uxcDzm3*$Vslj5TSzKv zW(1AYZE(~U!O8*ioAEf2={7Q%n-wQ%j9-1P^b7M66+FlmN)eud*Af z&cv$jW~k0*JH={3aXpv1+!6n8pZSvNyAsRj$0=Smmxf>Y{27~TP|DVCqc6mT>?tWZ zJ&6VY7ymAI2iv_i1MY1!2#Gdd@0us4Rw-&()o>TPJ9~l9CLULN@=?O{9B!XBgPlhrhy`4-<;!ZbWCTY!>=8vG=8Pqy0;R9!2AVM9vT}HDEJVLF^juW(85c@;uTK zU&qq9a^<7#V;YLxTItHTx=mxy-}3jp1Q4B3u+eKbZcjD+%tMcr9!d&0<#T5K>Tg?& zX$HK8#CQ%&1Pg2*@Bo#52Av_%UPH}kipghmeWv$kNVTE!PL zzw^dYH6^we^`DDxRfdEfpK?6}f?0GA1p%j)@5+#(pHrA|)rE+=D=a+zEYajB(4kJ% zcBYAlQ=(-234eY&q+#upcBAl!#nsV-%q$%o&rWy30a_P?plU4w> z!xBx3aq7HXP}BgpBO@~mY-6Lrf0r+7OTwxYQF|8U%|((YNu(c zMhNPVL>TOgMM5L}TcKA%YW{9p;8-XB>2FWizswS+7^|5OIBsk-)}bil;cD|Rj0V}t z$&=Eh?qS^^7x1}lvBtX8^@f)i+(*IW`W_?VMj$dhAxbWaXXc=&$1}8}!$b(DF+Smcn zYfcTSWp78R&bs|UMn&(&NjvkZV|c8bc{2IioYs09RR9gcXAycAinm?byPxmUK>T(l z7%cJ{st`2jyj^Wi1(lzi)w?Rq)J3D3VXYYa7|>C_ub4Hrs_=WwD&BzDTul&@NYfAR z9o`fm`S|IK(e`PbV|U9BYk%Dg7|8JJZjkKYAFDi}dOM&n5ZNfCyDEhlmzUbH)SOqD zU*xEO>-%KH!~n^fA|uRV2*aDyFHsK<2tN5l@SvOV%dw6z5g^n1-vt%T3^|5bwJws( znorzCs5iKl($rOyiN$vQkozf`~4GMrZ4apV8nhnpI4w5 z)Ye`Zd!j-_*Irzw0d{&6VNxgfvJkwq>3sERO3o0cls07A%9@d0d@Z7T_Yoq*=RT@| zzTF@Q1TV-l?-E0({0lg^%ooeU*aI)4!(Tn$&@*%m8{3@Tu$QZ`{5;F5Y@wgOLO)%u zARxD<)#dhUtfn=>ssQ^g>7>ys{_YZkI)dI_nXTV2-=4bPvfl1SG=Usm|2oeDRNc&P z(;q2f$QKViP7k2R%Y4QHh9 zK!Zgh?`HB9L^1liD-z)V{M)^Wzli$*7omhG+*u!9liyVL4>tPdfNPa3m_?*+{4?MT zey6(b1ps{v{zJ$>I(sT|Wfpn&o1mNkCUwfag|QlKkqIOd)8Mt?Q~V&KUL7KNaa+SR z{p(dC5)Rb{IqnMdD_E?EF^nHU)3h#+QmvcjdDrqUw4~NmA_T^xh!j0`;BHt-^9ob& zWKyCM>oBeM9W&FEd?r{YGo+@^phMhzM=||YPBc>W-5zD++`pHDQ#&S3W+0cJkT9y{?;19LXDfca(v~#Tl)?v|q|Ing5XJd?H3XcG+Kd*v0gl-Q6pMG*+3^*pT<);4iX2fn0`-s&Q8^gz=C2 zVDjnEp*-?c3vbR5f6GZhW5CVd;99kNy7Z;7xfYgEJ;ZV>yTI0oyFzkFrzblm%riQt z_fts6I*AD#=SwXTIq0`DSHf7nGgx61>BQgH*lt|rsL>-fe_+ynhiYGqoZY9NtnSUT z&MhY%w#l%uN8>Pf_D6nMj}*dCu%t})0~e}#>tD!u!8@y}@{bdi-<3f67g{9~Ts0nH z)$#)^Y-X!S=>}?h{lrDk5F)*5{I8Fo2sS$y&w$l#SC^ z5C8js_XJ)<_>Q;|=>*J<=QMhs*Ic<;18$0yl^Dsa<YjXdHGheIuzVHP^2UMn=Whs2ZZ(&gPIZRI z`?-_1^3D->xQ!=F*@;ER-Lx=b*!<_k_y7?EZ0MG?$+~FD8>@X~AB#3zh=$Ph_L@+z zcE!y^yZ{lcfO#vJ%f1vrTvm97GQn%_F@v3z)h?q`{H9m+Sae)`^z*8r;)9)!JfqvG zTJGhP<7X#u&LiAqytNR{AbHuKc-6{H&T+`6Eg3O-ML3NrQ=lIkY?Q8~ZM{!Irl zLvwV?zQmnq-Nh~cSbdc|IT7v7=w#5V+td2y%X(T)-(R0zO6D?rU7FZ)*YyEs-9{xF zDpofHcf?L{9U~}n&`@yIB?af+-y`IYM?F*U$Gb$J4u2U)e*0DHd@%t=or>9f)7=zM zDhaI0JC!2M1kV`f{2AkP{C(MDV(P(PHT))#QSl2~dg>s*;0H59w0`7r$Iug4B@0I6L zb3%Hl_ASgp&P8

8}|9X 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json new file mode 100644 index 00000000000..71d9667c0e2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zoomOut.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png b/submodules/TelegramUI/Images.xcassets/Media Editor/ZoomOut.imageset/zoomOut.png new file mode 100644 index 0000000000000000000000000000000000000000..7cb307c9bab184e043302265fa8c48d207046b96 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Wq?nJt9yaZf3iTW%`{)2AwnfV ze!&b12?FyC92(x=k1q%~zkmIN1^m3v{xL8xvU$2VhE&A8y=Gl@*nr37BBQ;K;9r?H z^&359{1sPvT0GzJNIUS35W~Ma877;G)?9G@*72j^o=D-;S97cu$b_Gaa(Ou+&1*Sh zO6LR}!Js9ctr}Z@wTL;bn!>U2l9$IMo*5jTlcZO1BvyJZbMn|9G*xlUTg{gOS>53Q zs^JTE{Ve}oH|6-Bl@4;-rK~pX2)}T-uK3P{$+o9bP0l+XkK4K2+c literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/Contents.json new file mode 100644 index 00000000000..e0629bed225 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/arrow (2).pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/AlertArrow.imageset/arrow (2).pdf new file mode 100644 index 0000000000000000000000000000000000000000..14f4cc754194c92e0a78fc8e13d9089f7dba0a00 GIT binary patch literal 2592 zcmd^BOHbS|5Wf3Y_<{rnHnH=vCA1Rk!m4T!bhig2)I-RcWoaJUBwIm$eP^85Nt$h; z=lTH7XV2>!&&(d(&aY0{BqD@SmlhAtgwnG!>U??KDeBzaJUpwU1`L+-N#&dRiE_BO zY@tP2>~}o@{oR~tnMQC0`pP)3c5$il(&avPDav>(41`BnG~tp<-=|>j5H=D{2v~Pz z6nYF*28;;_(j68|L?j~5MU6%}nD|0)H>AlSe^T;9B13>Qp=E%z^1_IxL>Sw`n6Zd# zoC&CvlR9E1!R%N+TJ2oq=4#f9j~k7>@Qm3`lo{`UnvO*6r+}wzY3&e0%RsONts_JK zn58fyA?8d-kM?*O$DAIfy63l5iKeVlOByg?GuZRWVm9vdV5Y_j(aa;s$!9`TzHaLR zc3AraYrAOzkzjbPQO}}pvbO?={F3Q^tLwP>rgzOW!OubO??%LM z=PW2bxpv45&qiMkOqcU?_Y}Xa+Dwp<4g}hfXCwKOy6LnD8hXk769GE=ak~arDMqlQ#^YJB2U#}qzXGJEiMaz+7-3~C zc9GBGI!=qtQ1+!%kHn>nk*0F+FAamh3n<|2LIb!c%x=Ay>w){YdQb>uFoXhs42AHR zhquI7P>yd&0fjjbyc)LDnD0Y@b0sJC; cI$zFe`%@toKyFpXWv$uaTrzTWba`|23n|AkQ2+n{ literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/Contents.json new file mode 100644 index 00000000000..64760fc91b1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "suggestphoto_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/suggestphoto_30.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/SuggestAvatar.imageset/suggestphoto_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..58874b25e4e2958dc7942e4966c1d0df0559c9f7 GIT binary patch literal 11665 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 61.0 { + displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps) + } + } + + self.displayLink = displayLink + displayLink.add(to: .main, forMode: .common) + displayLink.isPaused = false + } + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } +} + @objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate { @objc var window: UIWindow? + private var animationSupportContext: AnimationSupportContext? var nativeWindow: (UIWindow & WindowHost)? var mainWindow: Window1! private var dataImportSplash: LegacyDataImportSplash? @@ -352,6 +408,9 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) + |> deliverOnMainQueue).start(next: { sharedApplicationContext in + let _ = (sharedApplicationContext.sharedContext.activeAccountContexts + |> take(1) + |> deliverOnMainQueue).start(next: { activeAccounts in + var signals: Signal = .complete() + + for (_, context, _) in activeAccounts.accounts { + signals = signals |> then(context.account.cleanupTasks(lowImpact: false)) + } + + disposable.set(signals.start(completed: { + task.setTaskCompleted(success: true) + })) + }) + }) + + task.expirationHandler = { + disposable.dispose() + task.setTaskCompleted(success: false) + } + } + + BGTaskScheduler.shared.getPendingTaskRequests(completionHandler: { tasks in + if tasks.contains(where: { $0.identifier == taskId }) { + Logger.shared.log("App \(self.episodeId)", "Already have a cleanup task pending") + return + } + let request = BGProcessingTaskRequest(identifier: taskId) + request.requiresExternalPower = true + request.requiresNetworkConnectivity = false + + do { + try BGTaskScheduler.shared.submit(request) + } catch let e { + Logger.shared.log("App \(self.episodeId)", "Error submitting background task request: \(e)") + } + }) + } + return true } diff --git a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift index 39b4f421546..9d70c36fd27 100644 --- a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift @@ -122,12 +122,28 @@ final class ChatAvatarNavigationNode: ASDisplayNode { guard let strongSelf = self else { return } - let cachedPeerData = peerView.cachedData - if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo { - if let photo = maybePhoto, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { + let cachedPeerData = peerView.cachedData as? CachedUserData + var personalPhoto: TelegramMediaImage? + var profilePhoto: TelegramMediaImage? + var isKnown = false + + if let cachedPeerData = cachedPeerData { + if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto { + personalPhoto = maybePersonalPhoto + isKnown = true + } + if case let .known(maybePhoto) = cachedPeerData.photo { + profilePhoto = maybePhoto + isKnown = true + } + } + + if isKnown { + let photo = personalPhoto ?? profilePhoto + if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) if videoContent.id != strongSelf.videoContent?.id { strongSelf.videoNode?.removeFromSupernode() strongSelf.videoContent = videoContent @@ -157,7 +173,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode { strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil } - + strongSelf.updateVideoVisibility() } else { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() diff --git a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift index 4706b3ea449..924c7480feb 100644 --- a/submodules/TelegramUI/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Sources/ChatBotInfoItem.swift @@ -13,6 +13,7 @@ import AccountContext import UniversalMediaPlayer import TelegramUniversalVideoContent import WallpaperBackgroundNode +import ChatControllerInteraction private let messageFont = Font.regular(17.0) private let messageBoldFont = Font.semibold(17.0) @@ -139,6 +140,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { let videoContent = NativeVideoContent( id: .message(0, MediaId(namespace: 0, id: Int64.random(in: 0.. (UIView?, UIView?))? + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let result = itemNode.transitionNode(id: message.id, media: image) { + selectedNode = result + } + } + } + let transitionView = selectedNode?.0.view + + let senderName: String? + if let peer = message.peers[message.id.peerId] { + senderName = EnginePeer(peer).compactDisplayTitle + } else { + senderName = nil + } + + legacyAvatarEditor(context: strongSelf.context, media: .message(message: MessageReference(message), media: image), transitionView: transitionView, senderName: senderName, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, imageCompletion: { [weak self] image in + if let strongSelf = self { + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfilePhoto(image, mode: .accept) + } + } + }, videoCompletion: { [weak self] image, url, adjustments in + if let strongSelf = self { + if let rootController = strongSelf.effectiveNavigationController as? TelegramRootController, let settingsController = rootController.accountSettingsController as? PeerInfoScreenImpl { + settingsController.updateProfileVideo(image, mode: .accept, asset: AVURLAsset(url: url), adjustments: adjustments) + } + } + }) + } else { + openMessageByAction = true + } + } default: break } @@ -986,17 +1031,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(), snapshots: snapshots, transitionCompletion: { transitionCompletion() - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }, sendMessagesWithSignals: { [weak self] signals, _, _ in @@ -1192,7 +1226,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var existingIds = Set() for (_, file) in files { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { packReferences.append(packReference) existingIds.insert(id) @@ -1466,7 +1500,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var existingIds = Set() for (_, file) in customEmoji { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { packReferences.append(packReference) existingIds.insert(id) @@ -1632,6 +1666,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + if strongSelf.context.sharedContext.immediateExperimentalUISettings.disableQuickReaction { + itemNode.openMessageContextMenu() + return + } + let chosenReaction: MessageReaction.Reaction? switch reaction { @@ -3779,18 +3818,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { - legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } - }, getCaptionPanelView: { [weak self] in + legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }, sendMessagesWithSignals: { [weak self] signals, _, _ in if let strongSelf = self { @@ -9627,7 +9655,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let hapticFeedback = HapticFeedback() hapticFeedback.impact() - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action), elevatedLayout: false, action: { [weak self] action in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in guard let strongSelf = self else { return true } @@ -10254,6 +10282,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } // + // MARK: Nicegram Unblock + if let peer = self.presentationInterfaceState.renderedPeer?.peer { + let isLastSeenBlockedChat = (peer.id.id._internalGetInt64Value() == AppCache.lastSeenBlockedChatId) + if isLastSeenBlockedChat, + isAllowedChat(peer: peer, contentSettings: context.currentContentSettings.with { $0 }) { + AppCache.lastSeenBlockedChatId = nil + if #available(iOS 14.0, *) { + if let windowScene = self.view.window?.windowScene { + SKStoreReviewController.requestReview(in: windowScene) + } + } else { + SKStoreReviewController.requestReview() + } + } + } + // + self.didAppear = true self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false @@ -10447,7 +10492,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G for i in 0 ..< min(1, result.count) { if let video = result[i].videoRepresentations.first { let duration: Double = (video.representation.startTimestamp ?? 0.0) + (i == 0 ? 4.0 : 2.0) - signals.append(preloadVideoResource(postbox: context.account.postbox, resourceReference: video.reference, duration: duration)) + signals.append(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: video.reference, duration: duration)) } } return combineLatest(signals) |> mapToSignal { _ in @@ -12042,17 +12087,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G done(time) }) } - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }, dismissedWithResult: { [weak self] in @@ -12191,8 +12225,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (context.engine.messages.getAttachMenuBot(botId: botId) |> deliverOnMainQueue).start(next: { bot in let peer = EnginePeer(bot.peer) - let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, completion: { - let _ = (context.engine.messages.addBotToAttachMenu(botId: botId) + + let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in }, completed: { @@ -12404,10 +12439,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> take(1) |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in var stableId: String? - let queryPhoneNumber = formatPhoneNumber(phoneNumber) + let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) outer: for (id, data) in basicData { for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { stableId = id break outer } @@ -12683,17 +12718,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G done(time) }) } - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }) @@ -12781,17 +12805,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }) } - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }, present: { [weak self] c, a in @@ -12900,7 +12913,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) var previewRepresentations: [TelegramMediaImageRepresentation] = [] if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var attributes: [TelegramMediaFileAttribute] = [] attributes.append(.FileName(fileName: item.fileName)) @@ -12993,18 +13006,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } - controller.presentStickers = { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } - } controller.presentSchedulePicker = { [weak self] media, done in if let strongSelf = self { strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time in @@ -13090,18 +13091,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) })) - controller.presentStickers = { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } - } controller.getCaptionPanelView = { [weak self] in return self?.getCaptionPanelView() } @@ -13137,17 +13126,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G done(time) }) } - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView() }) @@ -13194,18 +13172,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) })) - controller.presentStickers = { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.present(controller, in: .window(.root)) - return controller - } else { - return nil - } - } controller.getCaptionPanelView = { [weak self] in return self?.getCaptionPanelView() } @@ -13314,10 +13280,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> take(1) |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in var stableId: String? - let queryPhoneNumber = formatPhoneNumber(phoneNumber) + let queryPhoneNumber = formatPhoneNumber(context: context, number: phoneNumber) outer: for (id, data) in basicData { for phoneNumber in data.phoneNumbers { - if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber { + if formatPhoneNumber(context: context, number: phoneNumber.value) == queryPhoneNumber { stableId = id break outer } @@ -13824,7 +13790,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var stickerPackReference: StickerPackReference? for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, _, _, packReference) = attribute { stickerPackReference = packReference break } @@ -14103,8 +14069,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] settings in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - strongSelf.chatDisplayNode.dismissInput() - + strongSelf.chatDisplayNode.dismissInput() let controller = mediaPasteboardScreen( context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, @@ -15510,7 +15475,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var attemptSelectionImpl: ((Peer) -> Void)? let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, attemptSelection: { peer, _ in attemptSelectionImpl?(peer) - }, multipleSelection: true, forwardedMessageIds: messages.map { $0.id })) + }, multipleSelection: true, forwardedMessageIds: messages.map { $0.id }, selectForumThreads: true)) let context = self.context attemptSelectionImpl = { [weak self, weak controller] peer in guard let strongSelf = self, let controller = controller else { @@ -15898,7 +15863,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break case let .chat(textInputState, _, _): if let textInputState = textInputState { - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData)) + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, selectForumThreads: true)) controller.peerSelected = { [weak self, weak controller] peer, threadId in let peerId = peer.id @@ -16398,58 +16363,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.commitPurposefulAction() let _ = self.presentVoiceMessageDiscardAlert(action: { - if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback { - if url.hasSuffix(".m3u8") { - let navigationController = self.navigationController as? NavigationController - - let webPage = TelegramMediaWebpage( - webpageId: MediaId(namespace: 0, id: 0), - content: .Loaded(TelegramMediaWebpageLoadedContent( - url: url, - displayUrl: url, - hash: 0, - type: "video", - websiteName: nil, - title: nil, - text: nil, - embedUrl: url, - embedType: "video", - embedSize: nil, - duration: nil, - author: nil, - image: nil, - file: nil, - attributes: [], - instantPage: nil - )) - ) - let entry = InstantPageGalleryEntry( - index: 0, - pageId: webPage.webpageId, - media: InstantPageMedia( - index: 0, - media: webPage, - url: nil, - caption: nil, - credit: nil - ), - caption: nil, - credit: nil, - location: nil - ) - - let gallery = InstantPageGalleryController(context: self.context, webPage: webPage, entries: [entry], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in - if let navigationController = navigationController { - navigationController.replaceTopController(controller, animated: false, ready: ready) - } - }, baseNavigationController: navigationController) - self.present(gallery, in: .window(.root), with: InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in - return nil - })) - return; - } - } - openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index c6c80226785..104436c2f14 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1,5 +1,8 @@ // MARK: Nicegram OpenGifsShortcut import EntityKeyboard +import NGAppCache +import NGRemoteConfig +import NGStrings // import Foundation import UIKit @@ -24,6 +27,9 @@ import ChatPresentationInterfaceState import ChatInputPanelContainer import PremiumUI import ChatTitleView +import ChatInputNode +import ChatEntityKeyboardInputNode +import ChatControllerInteraction final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -2351,9 +2357,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } // MARK: Nicegram + var showUnblockButton: Bool = false if (restrictionText != chatPresentationInterfaceState.strings.Channel_ErrorAccessDenied || restrictionText != chatPresentationInterfaceState.strings.Group_ErrorAccessDenied) { - if (isAllowedChat(peer: chatPresentationInterfaceState.renderedPeer?.peer, contentSettings: context.currentContentSettings.with { $0 })) { + let peer = chatPresentationInterfaceState.renderedPeer?.peer + if (isAllowedChat(peer: peer, contentSettings: context.currentContentSettings.with { $0 })) { restrictionText = nil + } else if restrictionText != nil { + showUnblockButton = true + AppCache.lastSeenBlockedChatId = peer?.id.id._internalGetInt64Value() } } @@ -2362,6 +2373,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { restrictionText = l("NGWeb.Blocked", chatPresentationInterfaceState.strings.baseLanguageCode) } } + + if hideUnblock { + showUnblockButton = false + } // if let restrictionText = restrictionText { if self.restrictedNode == nil { @@ -2370,6 +2385,19 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.restrictedNode = restrictedNode } self.restrictedNode?.setup(title: "", text: processedPeerRestrictionText(restrictionText)) + // MARK: Nicegram Unblock + if showUnblockButton { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.restrictedNode?.setupButton( + title: l("NicegramSettings.Unblock.Header", presentationData.strings.baseLanguageCode), + handler: { + UIApplication.shared.open(nicegramUnblockUrl) + } + ) + } else { + self.restrictedNode?.setupButton(title: nil, handler: nil) + } + // self.historyNodeContainer.isHidden = true self.navigateButtons.isHidden = true self.loadingNode.isHidden = true diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index d2563cf6e6a..16c5a797019 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -22,6 +22,7 @@ import ComponentFlow import ReactionSelectionNode import ChatPresentationInterfaceState import TelegramNotices +import ChatControllerInteraction extension ChatReplyThreadMessage { var effectiveTopId: MessageId { diff --git a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift index 66fccca20c9..f6797a8a976 100644 --- a/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistorySearchContainerNode.swift @@ -11,6 +11,7 @@ import AccountContext import SearchUI import TelegramUIPreferences import ListMessageItem +import ChatControllerInteraction private enum ChatHistorySearchEntryStableId: Hashable { case messageId(MessageId) diff --git a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift index d669d813d44..2ad88268334 100644 --- a/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputContextPanelNode.swift @@ -7,6 +7,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction enum ChatInputContextPanelPlacement { case overPanels diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift deleted file mode 100644 index bc62c8c9a0c..00000000000 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import ChatPresentationInterfaceState - -class ChatInputNode: ASDisplayNode { - var interfaceInteraction: ChatPanelInterfaceInteraction? - var ready: Signal { - return .single(Void()) - } - - var externalTopPanelContainer: UIView? { - return nil - } - - var topBackgroundExtension: CGFloat = 0.0 - var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? - - var hideInput: Bool = false - var adjustLayoutForHiddenInput: Bool = false - var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)? - - var followsDefaultHeight: Bool = false - - func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { - - } - - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { - return (0.0, 0.0) - } -} diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index fec662c9eeb..3b530b38bc3 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -3,6 +3,7 @@ import UIKit import TelegramCore import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult) -> (Int, Bool) { switch result { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 1da4d9b0aec..ec08704f02b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -5,6 +5,9 @@ import TelegramCore import Postbox import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction +import ChatInputNode +import ChatEntityKeyboardInputNode func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? { if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 7e5e257aa28..24c2418e41b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import TelegramCore import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: AccessoryPanelNode?, chatControllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { if let _ = chatPresentationInterfaceState.interfaceState.selectionState { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 27a071e37ae..fc264edf6fc 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -39,6 +39,7 @@ import Pasteboard import SettingsUI import PremiumUI import TextNodeWithEntities +import ChatControllerInteraction // MARK: Nicegram SelectAllMessagesWithAuthor // MARK: Nicegram ReplyPrivately @@ -422,7 +423,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let presentationData = context.sharedContext.currentPresentationData.with { $0 } var actions: [ContextMenuItem] = [] - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0)), badge: nil, icon: { theme in + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { c, _ in c.dismiss(completion: { @@ -432,7 +433,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) if !chatPresentationInterfaceState.isPremium && !premiumConfiguration.isPremiumDisabled { - actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0)), badge: nil, icon: { theme in + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { c, _ in c.dismiss(completion: { @@ -727,7 +728,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }, action: { _, f in f(.dismissWithoutContent) - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true)) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id @@ -1217,7 +1218,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if resourceAvailable, !message.containsSecretMedia { + if resourceAvailable, !message.containsSecretMedia, !chatPresentationInterfaceState.copyProtectionEnabled, !message.isCopyProtected() { var mediaReference: AnyMediaReference? var isVideo = false for media in message.media { @@ -1234,7 +1235,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: isVideo ? chatPresentationInterfaceState.strings.Gallery_SaveVideo : chatPresentationInterfaceState.strings.Gallery_SaveImage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, mediaReference: mediaReference) + let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) |> deliverOnMainQueue).start(completed: { Queue.mainQueue().after(0.2) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -2686,7 +2687,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus var firstCustomEmojiReaction: TelegramMediaFile? for (_, file) in customEmoji { loop: for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { if case let .id(id, _) = packReference, !existingIds.contains(id) { if firstCustomEmojiReaction == nil { firstCustomEmojiReaction = file diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 57dac75b3d5..a0e44743913 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -379,7 +379,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if alt == query { if !item.file.isPremiumEmoji || hasPremium { result.append((alt, item.file, alt)) @@ -443,7 +443,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee } for attribute in item.file.attributes { switch attribute { - case let .CustomEmoji(_, alt, _): + case let .CustomEmoji(_, _, alt, _): if !alt.isEmpty, let keyword = allEmoticons[alt] { if !item.file.isPremiumEmoji || hasPremium { result.append((alt, item.file, keyword)) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index fd5ac5aa90b..03b12d37b6f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -4,6 +4,7 @@ import TelegramCore import AccountContext import NGWebUtils import ChatPresentationInterfaceState +import ChatControllerInteraction func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatTitleAccessoryPanelNode?, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatTitleAccessoryPanelNode? { if case .overlay = chatPresentationInterfaceState.mode { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index f8d1b0a9143..062b1196afc 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -9,6 +9,10 @@ import TelegramPresentationData import ContextUI import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction +import MultiplexedVideoNode +import FeaturedStickersScreen +import ChatEntityKeyboardInputNode private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) { let searchBarHeight: CGFloat = 56.0 @@ -25,16 +29,6 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) { } } -final class ChatMediaInputGifPaneTrendingState { - let files: [MultiplexedVideoNodeFile] - let nextOffset: String? - - init(files: [MultiplexedVideoNodeFile], nextOffset: String?) { - self.files = files - self.nextOffset = nextOffset - } -} - final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { private let context: AccountContext private var theme: PresentationTheme diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift index c0502f3a5c3..195234e36b2 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGridEntries.swift @@ -6,6 +6,8 @@ import Display import TelegramPresentationData import MergeLists import ChatPresentationInterfaceState +import ChatControllerInteraction +import FeaturedStickersScreen enum ChatMediaInputGridEntryStableId: Equatable, Hashable { case search diff --git a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift index 0c4ee80922f..8e607099388 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputMetaSectionItemNode.swift @@ -8,6 +8,7 @@ import Postbox import TelegramPresentationData import AnimatedStickerNode import TelegramAnimatedStickerNode +import ChatPresentationInterfaceState enum ChatMediaInputMetaSectionItemType: Equatable { case savedStickers @@ -250,7 +251,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode { } animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) } else { self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black) let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 9910024c28f..b85b0db557f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -22,6 +22,12 @@ import ChatInterfaceState import ChatPresentationInterfaceState import UndoUI import PremiumUI +import ChatControllerInteraction +import FeaturedStickersScreen +import ChatInputNode +import FeaturedStickersScreen +import MultiplexedVideoNode +import ChatEntityKeyboardInputNode struct PeerSpecificPackData { let peer: Peer @@ -433,47 +439,6 @@ enum StickerPacksCollectionUpdate { case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) } -enum ChatMediaInputGifMode: Equatable { - case recent - case trending - case emojiSearch(String) -} - -final class ChatMediaInputNodeInteraction { - let navigateToCollectionId: (ItemCollectionId) -> Void - let navigateBackToStickers: () -> Void - let setGifMode: (ChatMediaInputGifMode) -> Void - let openSettings: () -> Void - let openTrending: (ItemCollectionId?) -> Void - let dismissTrendingPacks: ([ItemCollectionId]) -> Void - let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void - let openPeerSpecificSettings: () -> Void - let dismissPeerSpecificSettings: () -> Void - let clearRecentlyUsedStickers: () -> Void - - var stickerSettings: ChatInterfaceStickerSettings? - var highlightedStickerItemCollectionId: ItemCollectionId? - var highlightedItemCollectionId: ItemCollectionId? - var highlightedGifMode: ChatMediaInputGifMode = .recent - var previewedStickerPackItem: StickerPreviewPeekItem? - var appearanceTransition: CGFloat = 1.0 - var displayStickerPlaceholder = true - var displayStickerPackManageControls = true - - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { - self.navigateToCollectionId = navigateToCollectionId - self.navigateBackToStickers = navigateBackToStickers - self.setGifMode = setGifMode - self.openSettings = openSettings - self.openTrending = openTrending - self.dismissTrendingPacks = dismissTrendingPacks - self.toggleSearch = toggleSearch - self.openPeerSpecificSettings = openPeerSpecificSettings - self.dismissPeerSpecificSettings = dismissPeerSpecificSettings - self.clearRecentlyUsedStickers = clearRecentlyUsedStickers - } -} - func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> StickerPacksCollectionPosition { switch position { case let .scroll(index): diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputPane.swift index 012627463cf..183d06f6944 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPane.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Display import TelegramPresentationData +import ChatPresentationInterfaceState struct ChatMediaInputPaneScrollState { let absoluteOffset: CGFloat? diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift index 307b7020cab..56a8ba81ded 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPanelEntries.swift @@ -6,6 +6,7 @@ import Display import TelegramPresentationData import MergeLists import AccountContext +import ChatPresentationInterfaceState enum ChatMediaInputPanelAuxiliaryNamespace: Int32 { case savedStickers = 2 diff --git a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift index b2a78949212..1177f8871a1 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputPeerSpecificItem.swift @@ -8,6 +8,7 @@ import Postbox import TelegramPresentationData import AvatarNode import AccountContext +import ChatPresentationInterfaceState final class ChatMediaInputPeerSpecificItem: ListViewItem { let context: AccountContext diff --git a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift index c2d3ac7817b..0e1ececabd9 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputRecentGifsItem.swift @@ -6,6 +6,7 @@ import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData +import ChatPresentationInterfaceState final class ChatMediaInputRecentGifsItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction diff --git a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift index b374d760d8b..f8f1ecb1812 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputSettingsItem.swift @@ -6,6 +6,7 @@ import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData +import ChatPresentationInterfaceState final class ChatMediaInputSettingsItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index d55f0eeebb6..cbbb53a35f9 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -11,6 +11,8 @@ import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect +import ChatControllerInteraction +import ChatPresentationInterfaceState enum ChatMediaInputStickerGridSectionAccessory { case none @@ -290,12 +292,12 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0) if item.stickerItem.file.isVideoSticker { - self.imageNode.setSignal(chatMessageSticker(account: item.account, file: item.stickerItem.file, small: false, synchronousLoad: synchronousLoads && isVisible)) + self.imageNode.setSignal(chatMessageSticker(account: item.account, userLocation: .other, file: item.stickerItem.file, small: false, synchronousLoad: synchronousLoads && isVisible)) } else { - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(fittedSize))) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, userLocation: .other, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(fittedSize))) } self.updateVisibility() - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file), resource: item.stickerItem.file.resource).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, userLocation: .other, fileReference: stickerPackFileReference(item.stickerItem.file), resource: item.stickerItem.file.resource).start()) } else { if let animationNode = self.animationNode { animationNode.visibility = false @@ -304,8 +306,8 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { self.imageNode.isHidden = false self.didSetUpAnimationNode = false } - self.imageNode.setSignal(chatMessageSticker(account: item.account, file: item.stickerItem.file, small: !item.large, synchronousLoad: synchronousLoads && isVisible)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(item.stickerItem.file), resource: chatMessageStickerResource(file: item.stickerItem.file, small: !item.large)).start()) + self.imageNode.setSignal(chatMessageSticker(account: item.account, userLocation: .other, file: item.stickerItem.file, small: !item.large, synchronousLoad: synchronousLoads && isVisible)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, userLocation: .other, fileReference: stickerPackFileReference(item.stickerItem.file), resource: chatMessageStickerResource(file: item.stickerItem.file, small: !item.large)).start()) } self.currentState = (item.account, item.stickerItem, dimensions.cgSize) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift index 618f7b8d2aa..3c2ee14df60 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPackItem.swift @@ -11,6 +11,7 @@ import ItemListStickerPackItem import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect +import ChatPresentationInterfaceState final class ChatMediaInputStickerPackItem: ListViewItem { let account: Account @@ -200,7 +201,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } @@ -249,7 +250,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers } if let resourceReference = resourceReference { - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start()) + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference).start()) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerPane.swift index 024ede6514d..8c10f5497e6 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerPane.swift @@ -6,6 +6,8 @@ import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData +import ChatInputNode +import FeaturedStickersScreen private func fixGridScrolling(_ gridNode: GridNode) { var searchItemNode: GridItemNode? diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift index 5a440d6254e..03824271eba 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingItem.swift @@ -6,6 +6,7 @@ import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData +import ChatPresentationInterfaceState final class ChatMediaInputTrendingItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 8bc3bc39afe..3bfa2707927 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -239,8 +239,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.insertSubnode(imageNode, at: 0) strongSelf.insertSubnode(strongSelf.mediaBackgroundNode, at: 0) } - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: image), displayAtSize: nil, storeToDownloadsPeerType: nil).start()) - let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoads) + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoads) imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads) @@ -259,7 +259,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id { let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) + let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 17657d7514d..11803175ca3 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -28,6 +28,7 @@ import AppBundle import LottieMeshSwift import ChatPresentationInterfaceState import TextNodeWithEntities +import ChatControllerInteraction private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -564,13 +565,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if self.telegramFile?.id != telegramFile.id { self.telegramFile = telegramFile let dimensions = telegramFile.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: telegramFile, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)), thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), file: telegramFile, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)), thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) self.updateVisibility() - self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) + self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) if telegramFile.isPremiumSticker { if let effect = telegramFile.videoThumbnails.first { - self.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile), resource: effect.resource) .start()) + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), resource: effect.resource) .start()) } } } @@ -632,8 +633,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let fillSize = emojiFile.isCustomEmoji ? CGSize(width: 512.0, height: 512.0) : CGSize(width: 384.0, height: 384.0) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(fillSize), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) - self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start()) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), file: emojiFile, small: false, size: dimensions.cgSize.aspectFilled(fillSize), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad) + self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .standalone(media: emojiFile)).start()) } let textEmoji = item.message.text.strippedEmoji @@ -657,7 +658,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let animationItems = animationItems { for (_, animationItem) in animationItems { - self.disposables.add(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: animationItem.file)).start()) + self.disposables.add(freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .standalone(media: animationItem.file)).start()) } } } @@ -703,7 +704,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let overlayMeshAnimationNode = self.overlayMeshAnimationNode { self.overlayMeshAnimationNode = nil - if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { + if let transitionNode = item.controllerInteraction.getMessageTransitionNode() as? ChatMessageTransitionNode { transitionNode.remove(decorationNode: overlayMeshAnimationNode) } } @@ -1073,7 +1074,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) let font = Font.regular(fontSizeForEmojiString(item.message.text)) - let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: .black, linkColor: .black, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) + let textColor = item.presentationData.theme.theme.list.itemPrimaryTextColor + let attributedText = stringWithAppliedEntities(item.message.text, entities: item.message.textEntitiesAttribute?.entities ?? [], baseColor: textColor, linkColor: textColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font, message: item.message) textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural)) imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height) @@ -1946,7 +1948,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { guard let item = self.item else { return } - guard let transitionNode = item.controllerInteraction.getMessageTransitionNode() else { + guard let transitionNode = item.controllerInteraction.getMessageTransitionNode() as? ChatMessageTransitionNode else { return } @@ -2277,7 +2279,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if emoji.strippedEmoji == textEmoji.strippedEmoji { hasSound = true let mediaManager = item.context.sharedContext.mediaManager - let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) + let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), userContentType: .other, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) mediaPlayer.togglePlayPause() mediaPlayer.actionAtEnd = .action({ [weak self] in self?.mediaPlayer = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index f3fb5f65a81..38cfd555d9d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -18,6 +18,7 @@ import GalleryData import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer +import ChatControllerInteraction private let buttonFont = Font.semibold(13.0) @@ -618,7 +619,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { inlineImageDimensions = dimensions.cgSize if image != currentImage { - updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) + updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) } } } else if let image = media as? TelegramMediaWebFile { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index a470c66ff67..9b8df19d73e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -8,6 +8,7 @@ import TelegramUIPreferences import TelegramPresentationData import AccountContext import ChatMessageBackground +import ChatControllerInteraction enum ChatMessageBubbleContentBackgroundHiding { case never diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 3a749933611..b7c86684ac9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -48,6 +48,7 @@ import AnimationCache import MultiAnimationRenderer import ComponentFlow import EmojiStatusComponent +import ChatControllerInteraction enum InternalBubbleTapAction { case action(() -> Void) @@ -134,6 +135,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) } else if case .giftPremium = action.action { result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) + } else if case .suggestedProfilePhoto = action.action { + result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) } else { result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) } @@ -328,19 +331,6 @@ private enum ContentNodeOperation { case insert(index: Int, node: ChatMessageBubbleContentNode) } -class ChatPresentationContext { - weak var backgroundNode: WallpaperBackgroundNode? - let animationCache: AnimationCache - let animationRenderer: MultiAnimationRenderer - - init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) { - self.backgroundNode = backgroundNode - - self.animationCache = context.animationCache - self.animationRenderer = context.animationRenderer - } -} - private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize: CGSize, insets: UIEdgeInsets, to contentNode: ChatMessageBubbleContentNode) -> ListViewItemNodeVisibility { switch visibility { case .none: diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 17c792ff9c2..854016cb7bc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -13,6 +13,7 @@ import UniversalMediaPlayer import GalleryUI import HierarchyTrackingLayer import WallpaperBackgroundNode +import ChatControllerInteraction private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -535,7 +536,7 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { if let photo = maybePhoto, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) { let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, "\(Int32.random(in: 0 ..< Int32.max))"), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, "\(Int32.random(in: 0 ..< Int32.max))"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false) if videoContent.id != strongSelf.videoContent?.id { strongSelf.videoNode?.removeFromSupernode() strongSelf.videoContent = videoContent diff --git a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift index 3e59b91bdbd..f3367076ceb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGiftItemNode.swift @@ -16,6 +16,7 @@ import WallpaperBackgroundNode import ReactionSelectionNode import AnimatedStickerNode import TelegramAnimatedStickerNode +import ChatControllerInteraction private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, accountPeerId: PeerId) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 5b41b6856b1..e46afc445e8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -12,6 +12,7 @@ import AccountContext import LocalizedPeerData import ContextUI import Markdown +import ChatControllerInteraction private let nameFont = Font.medium(14.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 4d08a515d87..4b5c09324e4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -34,6 +34,7 @@ import TextSelectionNode import AudioTranscriptionPendingIndicatorComponent import UndoUI import TelegramNotices +import ChatControllerInteraction private struct FetchControls { let fetch: (Bool) -> Void @@ -575,7 +576,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if mediaUpdated { if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { - updateImageSignal = chatMessageImageFile(account: arguments.context.account, fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) + updateImageSignal = chatMessageImageFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } updatedFetchControls = FetchControls(fetch: { [weak self] userInitiated in diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 16ab0f01fc2..b6d75323bc1 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -612,7 +612,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let updatedFile = updatedFile, updatedMedia { if let resource = updatedFile.previewRepresentations.first?.resource { - strongSelf.fetchedThumbnailDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).start()) + strongSelf.fetchedThumbnailDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .peer(item.message.id.peerId), userContentType: .video, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).start()) } else { strongSelf.fetchedThumbnailDisposable.set(nil) } @@ -688,7 +688,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } } - }), content: NativeVideoContent(id: .message(item.message.stableId, telegramFile.fileId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, captureProtected: item.message.isCopyProtected()), priority: .embedded, autoplay: true) + }), content: NativeVideoContent(id: .message(item.message.stableId, telegramFile.fileId), userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, captureProtected: item.message.isCopyProtected()), priority: .embedded, autoplay: true) if let previousVideoNode = previousVideoNode { videoNode.bounds = previousVideoNode.bounds videoNode.position = previousVideoNode.position @@ -699,7 +699,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext if isSecretMedia { - let updatedSecretPlaceholderSignal = chatSecretMessageVideo(account: item.context.account, videoReference: .message(message: MessageReference(item.message), media: telegramFile)) + let updatedSecretPlaceholderSignal = chatSecretMessageVideo(account: item.context.account, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: telegramFile)) strongSelf.secretVideoPlaceholder.setSignal(updatedSecretPlaceholderSignal) if strongSelf.secretVideoPlaceholder.supernode == nil { strongSelf.insertSubnode(strongSelf.secretVideoPlaceholderBackground, belowSubnode: videoNode) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index dcdb9c14f13..1699ecfd74b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -23,6 +23,7 @@ import WallpaperResources import ChatMessageInteractiveMediaBadge import ContextUI import InvisibleInkDustNode +import ChatControllerInteraction private struct FetchControls { let fetch: (Bool) -> Void @@ -174,6 +175,7 @@ extension UIBezierPath { } private class ExtendedMediaOverlayNode: ASDisplayNode { + private let blurredImageNode: TransformImageNode private let dustNode: MediaDustNode private let buttonNode: HighlightTrackingButtonNode private let highlightedBackgroundNode: ASDisplayNode @@ -183,7 +185,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { private var maskView: UIView? private var maskLayer: CAShapeLayer? + private var randomId: Int32? + var isRevealed = false + var tapped: () -> Void = {} + override init() { + self.blurredImageNode = TransformImageNode() + self.blurredImageNode.contentAnimations = [] + self.dustNode = MediaDustNode() self.buttonNode = HighlightTrackingButtonNode() @@ -202,10 +211,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.textNode = ImmediateTextNode() super.init() - - self.clipsToBounds = true - self.isUserInteractionEnabled = false - + + self.addSubnode(self.blurredImageNode) self.addSubnode(self.dustNode) self.addSubnode(self.buttonNode) @@ -250,22 +257,72 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.maskLayer = maskLayer } - func update(size: CGSize, text: String, corners: ImageCorners?) { + func reveal() { + self.isRevealed = true + self.blurredImageNode.removeFromSupernode() + self.dustNode.removeFromSupernode() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if self.isRevealed { + return nil + } + return result + } + + func update(size: CGSize, text: String, imageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize, CGSize, Int32)?, imageFrame: CGRect, corners: ImageCorners?) { let spacing: CGFloat = 2.0 let padding: CGFloat = 10.0 + + if let (imageSignal, drawingSize, boundingSize, randomId) = imageSignal { + if self.randomId != randomId { + self.randomId = randomId + self.blurredImageNode.setSignal(imageSignal, attemptSynchronously: true) + + let imageLayout = self.blurredImageNode.asyncLayout() + let arguments = TransformImageArguments(corners: corners ?? ImageCorners(), imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .blurBackground, emptyColor: .clear, custom: nil) + let apply = imageLayout(arguments) + apply() + } + + self.blurredImageNode.isHidden = false + + self.isRevealed = self.dustNode.isRevealed + self.dustNode.revealed = { [weak self] in + self?.isRevealed = true + self?.blurredImageNode.removeFromSupernode() + } + self.dustNode.tapped = { [weak self] in + self?.isRevealed = true + self?.tapped() + } + } else { + self.blurredImageNode.isHidden = true + self.isRevealed = true + } + self.blurredImageNode.frame = imageFrame self.dustNode.frame = CGRect(origin: .zero, size: size) - self.dustNode.update(size: size, color: .white) + self.dustNode.update(size: size, color: .white, transition: .immediate) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center) - let textSize = self.textNode.updateLayout(size) - if let iconSize = self.iconNode.image?.size { - let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0) - self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) - self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) - - self.iconNode.frame = CGRect(origin: CGPoint(x: self.buttonNode.frame.minX + padding, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) - self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) + if text.isEmpty { + self.buttonNode.isHidden = true + self.textNode.isHidden = true + } else { + self.buttonNode.isHidden = false + self.textNode.isHidden = false + + self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center) + let textSize = self.textNode.updateLayout(size) + if let iconSize = self.iconNode.image?.size { + let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0) + self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) + self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) + + self.iconNode.frame = CGRect(origin: CGPoint(x: self.buttonNode.frame.minX + padding, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) + self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) + } } var leftOffset: CGFloat = 0.0 @@ -290,6 +347,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? private var currentHighQualityImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)? + private var currentBlurredImageSignal: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize, CGSize, Int32)? private var highQualityImageNode: TransformImageNode? private var videoNode: UniversalVideoNode? @@ -345,21 +403,28 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var visibilityPromise = ValuePromise(false, ignoreRepeated: true) var visibility: Bool = false { didSet { - if let videoNode = self.videoNode { - if self.visibility { - if !videoNode.canAttachContent { - videoNode.canAttachContent = true - if videoNode.hasAttachedContext { - videoNode.play() - } + self.updateVisibility() + } + } + + private var internallyVisible = true + private func updateVisibility() { + let visibility = self.visibility && self.internallyVisible + + if let videoNode = self.videoNode { + if visibility { + if !videoNode.canAttachContent { + videoNode.canAttachContent = true + if videoNode.hasAttachedContext { + videoNode.play() } - } else { - videoNode.canAttachContent = false } + } else { + videoNode.canAttachContent = false } - self.animatedStickerNode?.visibility = self.visibility - self.visibilityPromise.set(self.visibility) } + self.animatedStickerNode?.visibility = visibility + self.visibilityPromise.set(visibility) } var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } @@ -653,6 +718,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) var isExtendedMediaPreview = false var isInlinePlayableVideo = false var isSticker = false @@ -859,6 +925,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } var updateImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? + var updateBlurredImageSignal: ((Bool, Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>? var updatedFetchControls: FetchControls? @@ -944,11 +1011,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image)) } } else { updateImageSignal = { synchronousLoad, highQuality in - return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + return chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, highQuality: highQuality) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true, synchronousLoad: true) } } @@ -977,7 +1047,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio updatedFetchControls = FetchControls(fetch: { _ in if let strongSelf = self { - strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) + strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, userLocation: .peer(message.id.peerId), image: image).start()) } }, cancel: { chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image) @@ -985,22 +1055,25 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else if let file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = { synchronousLoad, _ in - return chatSecretMessageVideo(account: context.account, videoReference: .message(message: MessageReference(message), media: file)) + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) } } else { if file.isAnimatedSticker { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) updateImageSignal = { synchronousLoad, _ in - return chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) + return chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))) } } else if file.isSticker || file.isVideoSticker { updateImageSignal = { synchronousLoad, _ in - return chatMessageSticker(account: context.account, file: file, small: false) + return chatMessageSticker(account: context.account, userLocation: .peer(message.id.peerId), file: file, small: false) } } else { onlyFullSizeVideoThumbnail = isSendingUpdated updateImageSignal = { synchronousLoad, _ in - return mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + return mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) + } + updateBlurredImageSignal = { synchronousLoad, _ in + return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: true) } } } @@ -1051,7 +1124,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if file.isAnimated { - strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) + strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) } else { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, shouldSave: true).start()) } @@ -1072,7 +1145,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } else { var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) }) if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" { - representations.append(ImageRepresentationWithReference(representation: .init(dimensions: PixelDimensions(width: 1440, height: 2960), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource))) + representations.append(ImageRepresentationWithReference(representation: .init(dimensions: PixelDimensions(width: 1440, height: 2960), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource))) } if ["image/png", "image/svg+xml", "application/x-tgwallpattern"].contains(file.mimeType) { return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .screen) @@ -1235,7 +1308,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) let loopVideo = updatedVideoFile.isAnimated - let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia) + let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in @@ -1287,6 +1360,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } + + if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }), strongSelf.extendedMediaOverlayNode == nil { + strongSelf.internallyVisible = false + } + if let videoNode = strongSelf.videoNode { if !(replaceVideoNode ?? false), let decoration = videoNode.decoration as? ChatBubbleVideoDecoration, decoration.corners != corners { decoration.updateCorners(corners) @@ -1295,7 +1373,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size) - if strongSelf.visibility { + if strongSelf.visibility && strongSelf.internallyVisible { if !videoNode.canAttachContent { videoNode.canAttachContent = true if videoNode.hasAttachedContext { @@ -1327,9 +1405,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio if let imageDimensions = imageDimensions { strongSelf.currentHighQualityImageSignal = (updateImageSignal(false, true), imageDimensions) + + if let updateBlurredImageSignal = updateBlurredImageSignal { + strongSelf.currentBlurredImageSignal = (updateBlurredImageSignal(false, true), drawingSize, boundingSize, Int32.random(in: 0.. mapToSignal { visibility -> Signal in if visibility { @@ -1424,7 +1506,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio strongSelf.updateStatus(animated: synchronousLoads) - strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia && !isExtendedMediaPreview + strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia && !isExtendedMediaPreview && !hasSpoiler } }) }) @@ -1846,14 +1928,35 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio badgeNode.removeFromSupernode() } + var displaySpoiler = false if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia { + displaySpoiler = true + } else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { + displaySpoiler = true + } + + if displaySpoiler { if self.extendedMediaOverlayNode == nil { let extendedMediaOverlayNode = ExtendedMediaOverlayNode() + extendedMediaOverlayNode.tapped = { [weak self] in + self?.internallyVisible = true + self?.updateVisibility() + } self.extendedMediaOverlayNode = extendedMediaOverlayNode self.pinchContainerNode.contentNode.insertSubnode(extendedMediaOverlayNode, aboveSubnode: self.imageNode) } self.extendedMediaOverlayNode?.frame = self.imageNode.frame + var tappable = false + switch state { + case .play, .pause, .download, .none: + tappable = true + default: + break + } + + self.extendedMediaOverlayNode?.isUserInteractionEnabled = tappable + var paymentText: String = "" outer: for attribute in message.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute { @@ -1868,7 +1971,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio break } } - self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, corners: self.currentImageArguments?.corners) + self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) } else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { self.extendedMediaOverlayNode = nil extendedMediaOverlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak extendedMediaOverlayNode] _ in @@ -1936,6 +2039,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } func updateIsHidden(_ isHidden: Bool) { + if isHidden && !self.internallyVisible { + self.internallyVisible = true + self.updateVisibility() + self.extendedMediaOverlayNode?.reveal() + } + if let badgeNode = self.badgeNode, badgeNode.isHidden != isHidden { if isHidden { badgeNode.isHidden = true diff --git a/submodules/TelegramUI/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Sources/ChatMessageItem.swift index ce6cfbb335a..10b1f4dbe38 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItem.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import AccountContext import Emoji import PersistentStringHash +import ChatControllerInteraction public enum ChatMessageItemContent: Sequence { case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, attributes: ChatMessageEntryAttributes, location: MessageHistoryEntryLocation?) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index ba6ac55613e..6d25902beb5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -10,6 +10,7 @@ import ContextUI import ChatListUI import TelegramPresentationData import SwiftSignalKit +import ChatControllerInteraction struct ChatMessageItemWidthFill { var compactInset: CGFloat diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index b6860844a42..3e6a9218049 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -9,6 +9,7 @@ import TelegramUIPreferences import TelegramPresentationData import AccountContext import GridMessageSelectionNode +import ChatControllerInteraction class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override var supportsMosaic: Bool { diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 6a9bff67e6c..d28819382ac 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -350,12 +350,12 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let firstMessage = item.messages.first, let updatedMedia = updatedMedia, imageDimensions != nil { if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = mediaGridMessagePhoto(account: item.context.account, photoReference: .message(message: MessageReference(firstMessage), media: image)) + updateImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(firstMessage.id.peerId), photoReference: .message(message: MessageReference(firstMessage), media: image)) } else if let file = updatedMedia as? TelegramMediaFile { if file.isSticker { - updateImageSignal = chatMessageSticker(account: item.context.account, file: file, small: true, fetched: true) + updateImageSignal = chatMessageSticker(account: item.context.account, userLocation: .peer(firstMessage.id.peerId), file: file, small: true, fetched: true) } else if file.isVideo { - updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: .message(message: MessageReference(firstMessage), media: file), autoFetchFullSizeThumbnail: true) + updateImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(firstMessage.id.peerId), videoReference: .message(message: MessageReference(firstMessage), media: file), autoFetchFullSizeThumbnail: true) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift new file mode 100644 index 00000000000..313f4ed18fd --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatMessageProfilePhotoSuggestionContentNode.swift @@ -0,0 +1,319 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import TextFormat +import LocalizedPeerData +import TelegramStringFormatting +import WallpaperBackgroundNode +import ReactionSelectionNode +import PhotoResources +import UniversalMediaPlayer +import TelegramUniversalVideoContent +import GalleryUI +import Markdown + +class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode { + private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? + private let mediaBackgroundNode: NavigationBackgroundNode + private let subtitleNode: TextNode + private let imageNode: TransformImageNode + + fileprivate var videoNode: UniversalVideoNode? + private var videoContent: NativeVideoContent? + private var videoStartTimestamp: Double? + + private let buttonNode: HighlightTrackingButtonNode + private let buttonTitleNode: TextNode + + private var absoluteRect: (CGRect, CGSize)? + + private let fetchDisposable = MetaDisposable() + + required init() { + self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) + self.mediaBackgroundNode.clipsToBounds = true + self.mediaBackgroundNode.cornerRadius = 24.0 + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.displaysAsynchronously = false + + self.imageNode = TransformImageNode() + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.clipsToBounds = true + self.buttonNode.cornerRadius = 17.0 + + self.buttonTitleNode = TextNode() + self.buttonTitleNode.isUserInteractionEnabled = false + self.buttonTitleNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.mediaBackgroundNode) + self.addSubnode(self.subtitleNode) + self.addSubnode(self.imageNode) + + self.addSubnode(self.buttonNode) + self.addSubnode(self.buttonTitleNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonNode.alpha = 0.4 + strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.buttonTitleNode.alpha = 0.4 + } else { + strongSelf.buttonNode.alpha = 1.0 + strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.buttonTitleNode.alpha = 1.0 + strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable.dispose() + } + + override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + if self.item?.message.id == messageId { + return (self.imageNode, self.imageNode.bounds, { [weak self] in + guard let strongSelf = self else { + return (nil, nil) + } + + let resultView = strongSelf.imageNode.view.snapshotContentTree(unhide: true) + return (resultView, nil) + }) + } else { + return nil + } + } + + override func updateHiddenMedia(_ media: [Media]?) -> Bool { + var mediaHidden = false + var currentMedia: Media? + if let item = item { + mediaLoop: for media in item.message.media { + if let media = media as? TelegramMediaAction { + switch media.action { + case let .suggestedProfilePhoto(image): + currentMedia = image + break mediaLoop + default: + break + } + } + } + } + if let currentMedia = currentMedia, let media = media { + for item in media { + if item.isSemanticallyEqual(to: currentMedia) { + mediaHidden = true + break + } + } + } + + self.imageNode.isHidden = mediaHidden + return mediaHidden + } + + @objc private func buttonPressed() { + guard let item = self.item else { + return + } + let _ = item.controllerInteraction.openMessage(item.message, .default) + } + + override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let makeImageLayout = self.imageNode.asyncLayout() + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) + + let currentItem = self.item + + return { item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let width: CGFloat = 220.0 + let imageSize = CGSize(width: 100.0, height: 100.0) + + let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + + var photo: TelegramMediaImage? + if let media = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = media.action { + photo = image + } + + var mediaUpdated = true + if let photo = photo, let media = currentItem?.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(maybeCurrentPhoto) = media.action, let currentPhoto = maybeCurrentPhoto { + mediaUpdated = !photo.isSemanticallyEqual(to: currentPhoto) + } + + let isVideo = !(photo?.videoRepresentations.isEmpty ?? true) + let fromYou = item.message.author?.id == item.context.account.peerId + + let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? "" + let text: String + if fromYou { + text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoTextYou(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoTextYou(peerName).string + } else { + text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoText(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoText(peerName).string + } + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor) + + let subtitle = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in + return nil + }), textAlignment: .center) + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: isVideo ? item.presentationData.strings.Conversation_SuggestedVideoView : item.presentationData.strings.Conversation_SuggestedPhotoView, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 182.0) + + return (backgroundSize.width, { boundingWidth in + return (backgroundSize, { [weak self] animation, synchronousLoads, _ in + if let strongSelf = self { + strongSelf.item = item + + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize) + if let photo = photo { + if mediaUpdated { + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerType: nil).start()) + } + + let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), synchronousLoad: synchronousLoads) + strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads) + + let arguments = TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) + let apply = makeImageLayout(arguments) + apply() + + strongSelf.imageNode.frame = imageFrame + } + + if let photo = photo, let video = photo.videoRepresentations.last, let id = photo.id?.id { + let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) + if videoContent.id != strongSelf.videoContent?.id { + let mediaManager = item.context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) + videoNode.isUserInteractionEnabled = false + videoNode.ownsContentNodeUpdated = { [weak self] owns in + if let strongSelf = self { + strongSelf.videoNode?.isHidden = !owns + } + } + strongSelf.videoContent = videoContent + strongSelf.videoNode = videoNode + + videoNode.updateLayout(size: imageSize, transition: .immediate) + videoNode.frame = imageFrame + videoNode.clipsToBounds = true + videoNode.cornerRadius = imageFrame.width / 2.0 + + strongSelf.addSubnode(videoNode) + + videoNode.canAttachContent = true + if let videoStartTimestamp = video.startTimestamp { + videoNode.seek(videoStartTimestamp) + } else { + videoNode.seek(0.0) + } + videoNode.play() + + } + } else if let videoNode = strongSelf.videoNode { + strongSelf.videoContent = nil + strongSelf.videoNode = nil + + videoNode.removeFromSupernode() + } + + let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize) + strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame + + strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) + strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate) + strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) + + let _ = subtitleApply() + let _ = buttonTitleApply() + + let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 127.0), size: subtitleLayout.size) + strongSelf.subtitleNode.frame = subtitleFrame + + let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) + strongSelf.buttonTitleNode.frame = buttonTitleFrame + + let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) + + if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { + if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + strongSelf.mediaBackgroundNode.isHidden = true + backgroundContent.clipsToBounds = true + backgroundContent.allowsGroupOpacity = true + backgroundContent.cornerRadius = 24.0 + + strongSelf.mediaBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + + strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame + } else { + strongSelf.mediaBackgroundNode.isHidden = false + strongSelf.mediaBackgroundContent?.removeFromSupernode() + strongSelf.mediaBackgroundContent = nil + } + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + } + }) + }) + }) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + if let mediaBackgroundContent = self.mediaBackgroundContent { + var backgroundFrame = mediaBackgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.mediaBackgroundNode.frame.contains(point) { + return .openMessage + } else { + return .none + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 84ba80dde45..d6b74442ad7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -12,6 +12,7 @@ import AnimatedAvatarSetNode import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode +import ChatControllerInteraction func canViewMessageReactionList(message: Message) -> Bool { var found = false diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index fa7c78f7511..dcf630538ed 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -250,15 +250,17 @@ class ChatMessageReplyInfoNode: ASDisplayNode { mediaUpdated = true } + let hasSpoiler = arguments.message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if let updatedMediaReference = updatedMediaReference, mediaUpdated && imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: arguments.context.account, photoReference: imageReference) + updateImageSignal = chatMessagePhotoThumbnail(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: arguments.context.account, fileReference: fileReference) + updateImageSignal = chatMessageVideoThumbnail(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: arguments.context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + updateImageSignal = chatWebpageSnippetFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index da5210afb2d..b6b13a99553 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -13,6 +13,7 @@ import ContextUI import Markdown import ShimmerEffect import WallpaperBackgroundNode +import ChatControllerInteraction private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -256,10 +257,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { - let signal = chatMessageSticker(account: item.context.account, file: telegramFile, small: false, onlyFullSize: self.telegramFile != nil, synchronousLoad: synchronousLoad) + let signal = chatMessageSticker(account: item.context.account, userLocation: .peer(item.message.id.peerId), file: telegramFile, small: false, onlyFullSize: self.telegramFile != nil, synchronousLoad: synchronousLoad) self.telegramFile = telegramFile self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoad) - self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) + self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) } break diff --git a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift index 6ac4bedecb4..97aac1c46e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageSwipeToReplyNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import AppBundle import WallpaperBackgroundNode +import ChatControllerInteraction private let size = CGSize(width: 33.0, height: 33.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index d305d027ae3..159951cb98f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -334,7 +334,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil) var updatedAttributes: [NSAttributedString.Key: Any] = currentDict - updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor + //updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor updatedAttributes[ChatTextInputAttributes.customEmoji] = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) let insertString = NSAttributedString(string: updatedString.attributedSubstring(from: range).string, attributes: updatedAttributes) diff --git a/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift index 3fe396bfb18..e761792d15e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageThreadInfoNode.swift @@ -18,6 +18,7 @@ import MultiAnimationRenderer import ComponentFlow import EmojiStatusComponent import WallpaperBackgroundNode +import ChatControllerInteraction private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { enum CornerType { diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 8476cc2a3d5..25f4e00e88e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -9,6 +9,8 @@ import ContextUI import Postbox import TelegramCore import ReactionSelectionNode +import ChatControllerInteraction +import FeaturedStickersScreen private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect { if let presentationLayer = fromView.layer.presentation() { @@ -93,7 +95,7 @@ private final class OverlayTransitionContainerController: ViewController, Standa } } -public final class ChatMessageTransitionNode: ASDisplayNode { +public final class ChatMessageTransitionNode: ASDisplayNode, ChatMessageTransitionProtocol { static let animationDuration: Double = 0.3 static let verticalAnimationControlPoints: (Float, Float, Float, Float) = (0.19919472913616398, 0.010644531250000006, 0.27920937042459737, 0.91025390625) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 691833356b6..bc8cf1ed890 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -622,21 +622,23 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { mediaUpdated = true } + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedFetchMediaSignal: Signal? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference) + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isAnimatedSticker { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) - updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) - updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource)) + updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) + updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) } else if fileReference.media.isVideo || fileReference.media.isAnimated { - updateImageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference) + updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } else { diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift index 821cead160f..0279c92fd72 100644 --- a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -513,7 +513,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) @@ -1122,7 +1122,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDeleg let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file.file), reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) @@ -2150,7 +2150,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { guard let image = image, let videoFrame = videoFrame else { return } - renderVideo(context: context, backgroundImage: image, media: media, videoFrame: videoFrame, completion: { url in + renderVideo(context: context, backgroundImage: image, userLocation: .peer(message.id.peerId), media: media, videoFrame: videoFrame, completion: { url in if let url = url { completion(url) } @@ -2238,7 +2238,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { mediaFrame = CGRect(origin: CGPoint(x: 3.0, y: 63.0), size: mediaSize) if !wasInitialized { - self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: true, highQuality: true)) + self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: true, highQuality: true)) let imageLayout = self.imageNode.asyncLayout() let arguments = TransformImageArguments(corners: ImageCorners(radius: 0.0), imageSize: mediaSize, boundingSize: mediaSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .blurBackground, emptyColor: .black, custom: nil) @@ -2261,7 +2261,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode { videoSnapshotView.frame = mediaFrame } } else { - let videoContent = NativeVideoContent(id: .message(message.stableId, video.fileId), fileReference: .message(message: MessageReference(message), media: video), streamVideo: .conservative, loopVideo: true, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: self.isStatic, continuePlayingWithoutSoundOnLostAudioSession: true, placeholderColor: .clear, captureProtected: false) + let videoContent = NativeVideoContent(id: .message(message.stableId, video.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: video), streamVideo: .conservative, loopVideo: true, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: self.isStatic, continuePlayingWithoutSoundOnLostAudioSession: true, placeholderColor: .clear, captureProtected: false) let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay, autoplay: !self.isStatic) self.videoStatusDisposable.set((videoNode.status @@ -2394,8 +2394,8 @@ private enum RenderVideoResult { case error } -private func renderVideo(context: AccountContext, backgroundImage: UIImage, media: TelegramMediaFile, videoFrame: CGRect, completion: @escaping (URL?) -> Void) { - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: AnyMediaReference.standalone(media: media)) +private func renderVideo(context: AccountContext, backgroundImage: UIImage, userLocation: MediaResourceUserLocation, media: TelegramMediaFile, videoFrame: CGRect, completion: @escaping (URL?) -> Void) { + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: userLocation, mediaReference: AnyMediaReference.standalone(media: media)) |> deliverOnMainQueue).start(next: { value, isImage in guard case let .data(data) = value, data.complete else { return diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 04261503ed9..9e03899432e 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -26,6 +26,8 @@ import WallpaperBackgroundNode import BotPaymentsUI import ContextUI import Pasteboard +import ChatControllerInteraction +import ChatPresentationInterfaceState private final class ChatRecentActionsListOpaqueState { let entries: [ChatRecentActionsEntry] @@ -1013,7 +1015,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }), in: .current)*/ }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) case let .instantView(webpage, anchor): - strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) + strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor)) case let .join(link): strongSelf.presentController(JoinLinkPreviewController(context: strongSelf.context, link: link, navigateToPeer: { peer, peekData in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift index 3dbceb060f5..948ef2ff403 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsEmptyNode.swift @@ -1,3 +1,6 @@ +// MARK: Nicegram Unblock +import SolidRoundedButtonNode +// import Foundation import UIKit import Display @@ -25,6 +28,10 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { private var layoutParams: (CGSize, ChatPresentationData)? + // MARK: Nicegram Unblock + private let buttonNode: SolidRoundedButtonNode + // + private var title: String = "" private var text: String = "" @@ -40,6 +47,10 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false + // MARK: Nicegram Unblock + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0, gloss: true) + // + super.init() self.allowsGroupOpacity = true @@ -47,6 +58,9 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { self.addSubnode(self.backgroundNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + // MARK: Nicegram Unblock + self.addSubnode(self.buttonNode) + // } public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) { @@ -89,6 +103,22 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - titleLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top), size: titleLayout.size)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - textLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top + titleLayout.size.height + spacing), size: textLayout.size)) + // MARK: Nicegram Unblock + let buttonSize = CGSize( + width: size.width - insets.left - insets.right, + height: 50 + ) + let buttonFrame = CGRect( + origin: CGPoint( + x: (size.width - buttonSize.width) / 2, + y: backgroundFrame.maxY + 50 + ), + size: buttonSize + ) + transition.updateFrame(node: self.buttonNode, frame: buttonFrame) + let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) + // + let _ = titleApply() let _ = textApply() @@ -119,6 +149,14 @@ final class ChatRecentActionsEmptyNode: ASDisplayNode { } } + // MARK: Nicegram Unblock + func setupButton(title: String?, handler: (() -> Void)?) { + self.buttonNode.title = title + self.buttonNode.isHidden = (title == nil) + self.buttonNode.pressed = handler + } + // + func setup(title: String, text: String) { if self.title != title || self.text != text { self.title = title diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 288b6ddd8d9..f503332409f 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -6,6 +6,7 @@ import Postbox import TelegramPresentationData import MergeLists import AccountContext +import ChatControllerInteraction enum ChatRecentActionsEntryContentIndex: Int32 { case header = 0 @@ -1832,6 +1833,27 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .toggleAntiSpam(isEnabled): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + + let authorTitle: String = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + appendAttributedText(text: isEnabled ? self.presentationData.strings.Channel_AdminLog_AntiSpamEnabled(authorTitle) : self.presentationData.strings.Channel_AdminLog_AntiSpamDisabled(authorTitle), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index 42a2812bdff..35e4195931a 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -157,8 +157,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { } if let context = self.context { let mediaManager = context.sharedContext.mediaManager - let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) - mediaPlayer.actionAtEnd = .action{ [weak mediaPlayer] in + let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) + mediaPlayer.actionAtEnd = .action { [weak mediaPlayer] in mediaPlayer?.seek(timestamp: 0.0) } self.mediaPlayer = mediaPlayer diff --git a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift index 2bf23adc867..ec4627f166e 100644 --- a/submodules/TelegramUI/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Sources/ChatReplyCountItem.swift @@ -7,6 +7,7 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import WallpaperBackgroundNode +import ChatControllerInteraction private let titleFont = UIFont.systemFont(ofSize: 13.0) diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index e97bcfff6a8..c8de534d208 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -7,6 +7,7 @@ import TelegramPresentationData import ContextUI import ChatPresentationInterfaceState import ChatMessageBackground +import ChatControllerInteraction final class ChatTextInputActionButtonsNode: ASDisplayNode { private let presentationContext: ChatPresentationContext? diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 9497cdfadb1..9ecf2dd229b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -29,6 +29,7 @@ import LottieAnimationComponent import ComponentFlow import EmojiSuggestionsComponent import AudioToolbox +import ChatControllerInteraction private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -458,68 +459,6 @@ final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayC } } -final class CustomEmojiContainerView: UIView { - private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? - - private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] - - init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { - self.emojiViewProvider = emojiViewProvider - - super.init(frame: CGRect()) - } - - required init(coder: NSCoder) { - preconditionFailure() - } - - func update(fontSize: CGFloat, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { - var nextIndexById: [Int64: Int] = [:] - - var validKeys = Set() - for (rect, emoji) in emojiRects { - let index: Int - if let nextIndex = nextIndexById[emoji.fileId] { - index = nextIndex - } else { - index = 0 - } - nextIndexById[emoji.fileId] = index + 1 - - let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) - - let view: UIView - if let current = self.emojiLayers[key] { - view = current - } else if let newView = self.emojiViewProvider(emoji) { - view = newView - self.addSubview(newView) - self.emojiLayers[key] = view - } else { - continue - } - - let itemSize: CGFloat = floor(24.0 * fontSize / 17.0) - let size = CGSize(width: itemSize, height: itemSize) - - view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0) + 1.0), size: size) - - validKeys.insert(key) - } - - var removeKeys: [InlineStickerItemLayer.Key] = [] - for (key, view) in self.emojiLayers { - if !validKeys.contains(key) { - removeKeys.append(key) - view.removeFromSuperview() - } - } - for key in removeKeys { - self.emojiLayers.removeValue(forKey: key) - } - } -} - class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { // MARK: Nicegram Send with Enter let sendWithKb: Bool @@ -1045,7 +984,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } let pointSize = floor(24.0 * 1.3) - return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) + return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) } } } @@ -2401,7 +2340,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.customEmojiContainerView = customEmojiContainerView } - customEmojiContainerView.update(fontSize: fontSize, emojiRects: customEmojiRects) + customEmojiContainerView.update(fontSize: fontSize, textColor: textColor, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil @@ -2630,6 +2569,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition: .immediate, component: AnyComponent(EmojiSuggestionsComponent( context: context, + userLocation: .other, theme: theme, animationCache: presentationContext.animationCache, animationRenderer: presentationContext.animationRenderer, @@ -2648,7 +2588,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 883a7d58113..2575e6baae6 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -493,7 +493,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) @@ -901,7 +901,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file.file), reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start(next: { next in subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) diff --git a/submodules/TelegramUI/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Sources/ChatUnreadItem.swift index 84378321ba3..64b0a52b1b3 100644 --- a/submodules/TelegramUI/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Sources/ChatUnreadItem.swift @@ -7,6 +7,7 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import WallpaperBackgroundNode +import ChatControllerInteraction private let titleFont = UIFont.systemFont(ofSize: 13.0) diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 8b9f2f03456..8cb47032d5a 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -9,6 +9,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction private struct CommandChatInputContextPanelEntryStableId: Hashable { let command: PeerCommand diff --git a/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift index de6d79d3bca..250c514c001 100644 --- a/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandMenuChatInputContextPanelNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import MergeLists import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction private struct CommandMenuChatInputContextPanelEntryStableId: Hashable { let command: PeerCommand diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index f8e91fae5a6..7d2ac80b8d9 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -329,7 +329,7 @@ public func createChannelController(context: AccountContext) -> ViewController { if let data = image.jpegData(compressionQuality: 0.6) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) uploadedVideoAvatar = nil updateState { current in @@ -344,7 +344,7 @@ public func createChannelController(context: AccountContext) -> ViewController { if let data = image.jpegData(compressionQuality: 0.6) { let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) updateState { state in var state = state state.avatar = .image(representation, true) @@ -366,9 +366,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal - if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { let durationSignal: SSignal = SSignal(generator: { subscriber in let disposable = (entityRenderer.duration()).start(next: { duration in subscriber.putNext(duration) @@ -387,6 +385,8 @@ public func createChannelController(context: AccountContext) -> ViewController { } }) + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! } else { signal = SSignal.complete() } @@ -454,7 +454,7 @@ public func createChannelController(context: AccountContext) -> ViewController { } } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! let _ = currentAvatarMixin.swap(mixin) mixin.requestSearchController = { assetsController in let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index e9289fa880c..d7cbef2a798 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -601,7 +601,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] if let data = image.jpegData(compressionQuality: 0.6) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) uploadedVideoAvatar = nil updateState { current in @@ -616,7 +616,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] if let data = image.jpegData(compressionQuality: 0.6) { let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) updateState { state in var state = state state.avatar = .image(representation, true) @@ -639,9 +639,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal - if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { let durationSignal: SSignal = SSignal(generator: { subscriber in let disposable = (entityRenderer.duration()).start(next: { duration in subscriber.putNext(duration) @@ -660,6 +658,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } }) + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! } else { signal = SSignal.complete() } @@ -727,7 +727,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false)! + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! let _ = currentAvatarMixin.swap(mixin) mixin.requestSearchController = { assetsController in let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in diff --git a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift index 8a558045eb7..ff4f17d0b79 100644 --- a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift @@ -8,6 +8,7 @@ import TelegramStringFormatting import TelegramUIPreferences import AccountContext import ChatPresentationInterfaceState +import ChatControllerInteraction final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { private let containerNode: ASDisplayNode diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift deleted file mode 100644 index 72ecc4dc8c3..00000000000 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ /dev/null @@ -1,1415 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import Postbox -import TelegramCore -import SwiftSignalKit -import AccountContext -import TelegramPresentationData -import TelegramUIPreferences -import MergeLists -import StickerPackPreviewUI -import OverlayStatusController -import PresentationDataUtils -import SearchBarNode -import UndoUI -import SegmentedControlNode -import LegacyComponents -import ChatPresentationInterfaceState - -private enum DrawingPaneType { - case stickers - case masks -} - -private struct DrawingPaneArrangement { - let panes: [DrawingPaneType] - let currentIndex: Int - let indexTransition: CGFloat - - func withIndexTransition(_ indexTransition: CGFloat) -> DrawingPaneArrangement { - return DrawingPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: indexTransition) - } - - func withCurrentIndex(_ currentIndex: Int) -> DrawingPaneArrangement { - return DrawingPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: self.indexTransition) - } -} - -private final class DrawingStickersScreenNode: ViewControllerTracingNode { - private let context: AccountContext - private var presentationData: PresentationData - fileprivate var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? - private var searchItemContext = StickerPaneSearchGlobalItemContext() - private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> - - private let controllerInteraction: ChatControllerInteraction - private var stickersNodeInteraction: ChatMediaInputNodeInteraction! - private var masksNodeInteraction: ChatMediaInputNodeInteraction! - - private let collectionListPanel: ASDisplayNode - private let collectionListContainer: ASDisplayNode - - private let blurView: UIView - - private let topPanel: ASDisplayNode - private let segmentedControlNode: SegmentedControlNode - private let cancelButton: HighlightableButtonNode - private let topSeparatorNode: ASDisplayNode - private let bottomSeparatorNode: ASDisplayNode - - private let stickerListView: ListView - private let maskListView: ListView - private var hiddenListView: ListView? - - private var searchContainerNode: PaneSearchContainerNode? - private let searchContainerNodeLoadedDisposable = MetaDisposable() - - private let stickerPane: ChatMediaInputStickerPane - private let maskPane: ChatMediaInputStickerPane - private var hiddenPane: ChatMediaInputStickerPane? - - private let stickerItemCollectionsViewPosition = Promise() - private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? - private var currentStickerView: ItemCollectionsView? - - private let maskItemCollectionsViewPosition = Promise() - private var currentMaskPacksCollectionPosition: StickerPacksCollectionPosition? - private var currentMaskView: ItemCollectionsView? - - private var paneArrangement: DrawingPaneArrangement - - private var animatingStickerPaneOut = false - private var animatingMaskPaneOut = false - - private var panRecognizer: UIPanGestureRecognizer? - - private var validLayout: ContainerViewLayout? - - private var disposable = MetaDisposable() - private var maskDisposable = MetaDisposable() - - private let _ready = Promise() - var ready: Promise { - return self._ready - } - private var didSetReady: Bool = false - - fileprivate var dismiss: (() -> Void)? - - init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { - self.context = context - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationData = presentationData - self.selectSticker = selectSticker - - self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) - - var selectStickerImpl: ((FileMediaReference, UIView, CGRect) -> Bool)? - - self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in - }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in - }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendEmoji: { _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in - }, presentControllerInCurrent: { _, _ in - }, navigationController: { - return nil - }, chatControllerNode: { - return nil - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in - }, canSetupReply: { _ in - return .none - }, navigateToFirstDateMessage: { _, _ in - }, requestRedeliveryOfFailedMessages: { _ in - }, addContact: { _ in - }, rateCall: { _, _, _ in - }, requestSelectMessagePollOptions: { _, _ in - }, requestOpenMessagePollResults: { _, _ in - }, openAppStorePage: { - }, displayMessageTooltip: { _, _, _, _ in - }, seekToTimecode: { _, _, _ in - }, scheduleCurrentMessage: { - }, sendScheduledMessagesNow: { _ in - }, editScheduledMessagesTime: { _ in - }, performTextSelectionAction: { _, _, _ in - }, displayImportedMessageTooltip: { _ in - }, displaySwipeToReplyHint: { - }, dismissReplyMarkupMessage: { _ in - }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in - }, displayPollSolution: { _, _ in - }, displayPsa: { _, _ in - }, displayDiceTooltip: { _ in - }, animateDiceSuccess: { _, _ in - }, displayPremiumStickerTooltip: { _, _ in - }, displayEmojiPackTooltip: { _, _ in - }, openPeerContextMenu: { _, _, _, _, _ in - }, openMessageReplies: { _, _, _ in - }, openReplyThreadOriginalMessage: { _ in - }, openMessageStats: { _ in - }, editMessageMedia: { _, _ in - }, copyText: { _ in - }, displayUndo: { _ in - }, isAnimatingMessage: { _ in - return false - }, getMessageTransitionNode: { - return nil - }, updateChoosingSticker: { _ in - }, commitEmojiInteraction: { _, _, _, _ in - }, openLargeEmojiInfo: { _, _, _ in - }, openJoinLink: { _ in - }, openWebView: { _, _, _, _ in - }, activateAdAction: { _ in - }, requestMessageUpdate: { _, _ in - }, cancelInteractiveKeyboardGestures: { - }, dismissTextInput: { - }, scrollToMessageId: { _ in - }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: true), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) - - self.blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - - self.topPanel = ASDisplayNode() - self.topPanel.clipsToBounds = true - self.topPanel.backgroundColor = UIColor(rgb: 0x151515) - self.topPanel.alpha = 0.3 - - let segmentedTheme = SegmentedControlTheme(backgroundColor: UIColor(rgb: 0x2c2d2d), foregroundColor: UIColor(rgb: 0x656565), shadowColor: UIColor.clear, textColor: .white, dividerColor: .white) - self.segmentedControlNode = SegmentedControlNode(theme: segmentedTheme, items: [SegmentedControlItem(title: self.presentationData.strings.Paint_Stickers), SegmentedControlItem(title: self.presentationData.strings.Paint_Masks)], selectedIndex: 0) - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setAttributedTitle(NSAttributedString(string: self.presentationData.strings.Common_Cancel, font: Font.regular(17.0), textColor: .white), for: .normal) - - self.collectionListPanel = ASDisplayNode() - self.collectionListPanel.clipsToBounds = true - self.collectionListPanel.backgroundColor = UIColor(rgb: 0x151515) - self.collectionListPanel.alpha = 0.3 - - self.collectionListContainer = ASDisplayNode() - self.collectionListContainer.clipsToBounds = true - - self.stickerListView = ListView() - self.stickerListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) - self.stickerListView.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.maskListView = ListView() - self.maskListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) - self.maskListView.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(rgb: 0x2c2d2d) - - self.bottomSeparatorNode = ASDisplayNode() - self.bottomSeparatorNode.backgroundColor = UIColor(rgb: 0x2c2d2d) - - var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? - self.stickerPane = ChatMediaInputStickerPane(theme: self.presentationData.theme, strings: self.presentationData.strings, paneDidScroll: { pane, state, transition in - paneDidScrollImpl?(pane, state, transition) - }, fixPaneScroll: { pane, state in - - }) - - self.maskPane = ChatMediaInputStickerPane(theme: self.presentationData.theme, strings: self.presentationData.strings, paneDidScroll: { pane, state, transition in - paneDidScrollImpl?(pane, state, transition) - }, fixPaneScroll: { pane, state in - - }) - - self.paneArrangement = DrawingPaneArrangement(panes: [.stickers, .masks], currentIndex: 0, indexTransition: 0.0) - - super.init() - - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - - self.view.addSubview(self.blurView) - - self.stickersNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in - if let strongSelf = self, let currentView = strongSelf.currentStickerView, (collectionId != strongSelf.stickersNodeInteraction.highlightedItemCollectionId || true) { - var index: Int32 = 0 - if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { - strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( - context: strongSelf.context, - highlightedPackId: nil, - sendSticker: { - fileReference, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) - } else { - return false - } - } - )) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { - strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) - strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.stickerItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { - strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) - strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.stickerItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { - strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) - strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.stickerItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else { - strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) - for (id, _, _) in currentView.collectionInfos { - if id.namespace == collectionId.namespace { - if id == collectionId { - let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) - strongSelf.currentStickerPacksCollectionPosition = .navigate(index: itemIndex, collectionId: nil) - strongSelf.stickerItemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil))) - break - } - index += 1 - } - } - } - } - }, navigateBackToStickers: { - }, setGifMode: { _ in - }, openSettings: { - }, openTrending: { _ in - }, dismissTrendingPacks: { _ in - }, toggleSearch: { [weak self] value, searchMode, query in - if let strongSelf = self { - if let searchMode = searchMode, value { - var searchContainerNode: PaneSearchContainerNode? - if let current = strongSelf.searchContainerNode { - searchContainerNode = current - } else { - searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.stickersNodeInteraction, mode: searchMode, trendingGifsPromise: Promise(nil), cancel: { - self?.searchContainerNode?.deactivate() - self?.stickersNodeInteraction.toggleSearch(false, nil, "") - }) - strongSelf.searchContainerNode = searchContainerNode - if !query.isEmpty { - DispatchQueue.main.async { - searchContainerNode?.updateQuery(query) - } - } - } - if let searchContainerNode = searchContainerNode { - strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready - |> deliverOnMainQueue).start(next: { - if let strongSelf = self { - strongSelf.controllerInteraction.updateInputMode { current in - switch current { - case let .media(mode, _, focused): - return .media(mode: mode, expanded: .search(searchMode), focused: focused) - default: - return current - } - } - } - })) - } - } else { - strongSelf.controllerInteraction.updateInputMode { current in - switch current { - case let .media(mode, _, focused): - return .media(mode: mode, expanded: nil, focused: focused) - default: - return current - } - } - } - } - }, openPeerSpecificSettings: { - }, dismissPeerSpecificSettings: { - }, clearRecentlyUsedStickers: { [weak self] in - if let strongSelf = self { - let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: strongSelf.presentationData.theme, fontSize: strongSelf.presentationData.listsFontSize)) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - let _ = context.engine.stickers.clearRecentlyUsedStickers().start() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.controllerInteraction.presentController(actionSheet, nil) - } - }) - self.stickersNodeInteraction.stickerSettings = ChatInterfaceStickerSettings(loopAnimatedStickers: true) - self.stickersNodeInteraction.displayStickerPlaceholder = false - - self.masksNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in - if let strongSelf = self, let currentView = strongSelf.currentMaskView, (collectionId != strongSelf.masksNodeInteraction.highlightedItemCollectionId || true) { - var index: Int32 = 0 - if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { - strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( - context: strongSelf.context, - highlightedPackId: nil, - sendSticker: { - fileReference, sourceNode, sourceRect in - if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) - } else { - return false - } - } - )) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { - strongSelf.setCurrentPane(.masks, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) - strongSelf.currentMaskPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.maskItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { - strongSelf.setCurrentPane(.masks, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) - strongSelf.currentMaskPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.maskItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { - strongSelf.setCurrentPane(.masks, transition: .animated(duration: 0.25, curve: .spring)) - strongSelf.currentMaskPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) - strongSelf.maskItemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) - } else { - strongSelf.setCurrentPane(.masks, transition: .animated(duration: 0.25, curve: .spring)) - for (id, _, _) in currentView.collectionInfos { - if id.namespace == collectionId.namespace { - if id == collectionId { - let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) - strongSelf.currentMaskPacksCollectionPosition = .navigate(index: itemIndex, collectionId: nil) - strongSelf.maskItemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil))) - break - } - index += 1 - } - } - } - } - }, navigateBackToStickers: { - }, setGifMode: { _ in - }, openSettings: { - }, openTrending: { _ in - }, dismissTrendingPacks: { _ in - }, toggleSearch: { [weak self] value, searchMode, query in - if let strongSelf = self { - if let searchMode = searchMode, value { - var searchContainerNode: PaneSearchContainerNode? - if let current = strongSelf.searchContainerNode { - searchContainerNode = current - } else { - searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.masksNodeInteraction, mode: searchMode, trendingGifsPromise: Promise(nil), cancel: { - self?.searchContainerNode?.deactivate() - self?.masksNodeInteraction.toggleSearch(false, nil, "") - }) - strongSelf.searchContainerNode = searchContainerNode - if !query.isEmpty { - DispatchQueue.main.async { - searchContainerNode?.updateQuery(query) - } - } - } - if let searchContainerNode = searchContainerNode { - strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready - |> deliverOnMainQueue).start(next: { - if let strongSelf = self { - strongSelf.controllerInteraction.updateInputMode { current in - switch current { - case let .media(mode, _, focused): - return .media(mode: mode, expanded: .search(searchMode), focused: focused) - default: - return current - } - } - } - })) - } - } else { - strongSelf.controllerInteraction.updateInputMode { current in - switch current { - case let .media(mode, _, focused): - return .media(mode: mode, expanded: nil, focused: focused) - default: - return current - } - } - } - } - }, openPeerSpecificSettings: { - }, dismissPeerSpecificSettings: { - }, clearRecentlyUsedStickers: { [weak self] in - if let strongSelf = self { - let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: strongSelf.presentationData.theme, fontSize: strongSelf.presentationData.listsFontSize)) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - let _ = context.engine.stickers.clearRecentlyUsedStickers().start() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.controllerInteraction.presentController(actionSheet, nil) - } - }) - self.masksNodeInteraction.stickerSettings = ChatInterfaceStickerSettings(loopAnimatedStickers: true) - self.masksNodeInteraction.displayStickerPlaceholder = false - - self.addSubnode(self.topPanel) - - self.collectionListContainer.addSubnode(self.collectionListPanel) - self.addSubnode(self.collectionListContainer) - - self.addSubnode(self.segmentedControlNode) - self.addSubnode(self.cancelButton) - - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.bottomSeparatorNode) - - let trendingInteraction = TrendingPaneInteraction(installPack: { info in - }, openPack: { info in - }, getItemIsPreviewed: { item in - return false - }, openSearch: { - }) - - let stickerItemCollectionsView = self.stickerItemCollectionsViewPosition.get() - |> distinctUntilChanged - |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in - switch position { - case .initial: - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .initial - } else { - update = .generic - } - return (view, update) - } - case let .scroll(aroundIndex): - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 300) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .scroll - } else { - update = .generic - } - return (view, update) - } - case let .navigate(index, collectionId): - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 300) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .navigate(index, collectionId) - } else { - update = .generic - } - return (view, update) - } - } - } - - let maskItemCollectionsView = self.maskItemCollectionsViewPosition.get() - |> distinctUntilChanged - |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in - switch position { - case .initial: - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudMaskPacks], aroundIndex: nil, count: 50) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .initial - } else { - update = .generic - } - return (view, update) - } - case let .scroll(aroundIndex): - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudMaskPacks], aroundIndex: aroundIndex, count: 300) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .scroll - } else { - update = .generic - } - return (view, update) - } - case let .navigate(index, collectionId): - var firstTime = true - return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudMaskPacks], aroundIndex: index, count: 300) - |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .navigate(index, collectionId) - } else { - update = .generic - } - return (view, update) - } - } - } - - let controllerInteraction = self.controllerInteraction - let stickersInputNodeInteraction = self.stickersNodeInteraction! - - let previousStickerEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) - let previousStickerView = Atomic(value: nil) - - let stickerTransitions = combineLatest(queue: Queue(), stickerItemCollectionsView, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get()) - |> map { viewAndUpdate, trendingPacks, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in - let (view, viewUpdate) = viewAndUpdate - let previous = previousStickerView.swap(view) - var update = viewUpdate - if previous === view { - update = .generic - } - let (theme, strings) = themeAndStrings - - var savedStickers: OrderedItemListView? - var recentStickers: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { - recentStickers = orderedView - } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { - savedStickers = orderedView - } - } - - var installedPacks = Set() - for info in view.collectionInfos { - installedPacks.insert(info.0) - } - - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, theme: theme, strings: strings, hasGifs: false, hasSettings: false) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, trendingPacks: [], installedPacks: installedPacks, hasSearch: false, hasAccessories: false, strings: strings, theme: theme, hasPremium: false, isPremiumDisabled: true, trendingIsPremium: false) - - let (previousPanelEntries, previousGridEntries) = previousStickerEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: stickersInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: stickersInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) - } - - self.disposable.set((stickerTransitions - |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in - if let strongSelf = self { - strongSelf.currentStickerView = view - strongSelf.enqueuePanelTransition(listView: strongSelf.stickerListView, pane: strongSelf.stickerPane, transition: panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) - } - })) - - let masksInputNodeInteraction = self.masksNodeInteraction! - - let previousMaskEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [])) - let previousMaskView = Atomic(value: nil) - - let maskTransitions = combineLatest(queue: Queue(), maskItemCollectionsView, self.themeAndStringsPromise.get()) - |> map { viewAndUpdate, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in - let (view, viewUpdate) = viewAndUpdate - let previous = previousMaskView.swap(view) - var update = viewUpdate - if previous === view { - update = .generic - } - let (theme, strings) = themeAndStrings - - var installedPacks = Set() - for info in view.collectionInfos { - installedPacks.insert(info.0) - } - - let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, theme: theme, strings: strings, hasGifs: false, hasSettings: false) - let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: nil, recentStickers: nil, peerSpecificPack: nil, canInstallPeerSpecificPack: .none, trendingPacks: [], installedPacks: installedPacks, hasSearch: false, hasAccessories: false, strings: strings, theme: theme, hasPremium: false, isPremiumDisabled: true, trendingIsPremium: false) - - let (previousPanelEntries, previousGridEntries) = previousMaskEntries.swap((panelEntries, gridEntries)) - return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: masksInputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: masksInputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) - } - - self.maskDisposable.set((maskTransitions - |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in - if let strongSelf = self { - strongSelf.currentMaskView = view - strongSelf.enqueuePanelTransition(listView: strongSelf.maskListView, pane: strongSelf.maskPane, transition: panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) - } - })) - - self.stickerPane.gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self { - var topVisibleCollectionId: ItemCollectionId? - - if let topVisibleSection = visibleItems.topSectionVisible as? ChatMediaInputStickerGridSection { - topVisibleCollectionId = topVisibleSection.collectionId - } else if let topVisible = visibleItems.topVisible { - if let item = topVisible.1 as? ChatMediaInputStickerGridItem { - topVisibleCollectionId = item.index.collectionId - } else if let _ = topVisible.1 as? StickerPanePeerSpecificSetupGridItem { - topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) - } - } - if let collectionId = topVisibleCollectionId { - if strongSelf.stickersNodeInteraction.highlightedItemCollectionId != collectionId { - strongSelf.setHighlightedStickerItemCollectionId(collectionId) - } - } - - if let currentView = strongSelf.currentStickerView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom { - if topIndex <= 10 && currentView.lower != nil { - let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index)) - if strongSelf.currentStickerPacksCollectionPosition != position { - strongSelf.currentStickerPacksCollectionPosition = position - strongSelf.stickerItemCollectionsViewPosition.set(.single(position)) - } - } else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil { - var position: StickerPacksCollectionPosition? - if let bottomItem = bottomItem as? ChatMediaInputStickerGridItem { - position = clipScrollPosition(.scroll(aroundIndex: bottomItem.index)) - } - - if let position = position, strongSelf.currentStickerPacksCollectionPosition != position { - strongSelf.currentStickerPacksCollectionPosition = position - strongSelf.stickerItemCollectionsViewPosition.set(.single(position)) - } - } - } - } - } - - self.maskPane.gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self { - var topVisibleCollectionId: ItemCollectionId? - - if let topVisibleSection = visibleItems.topSectionVisible as? ChatMediaInputStickerGridSection { - topVisibleCollectionId = topVisibleSection.collectionId - } else if let topVisible = visibleItems.topVisible { - if let item = topVisible.1 as? ChatMediaInputStickerGridItem { - topVisibleCollectionId = item.index.collectionId - } else if let _ = topVisible.1 as? StickerPanePeerSpecificSetupGridItem { - topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) - } - } - if let collectionId = topVisibleCollectionId { - if strongSelf.masksNodeInteraction.highlightedItemCollectionId != collectionId { - strongSelf.setHighlightedMaskItemCollectionId(collectionId) - } - } - - if let currentView = strongSelf.currentMaskView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom { - if topIndex <= 10 && currentView.lower != nil { - let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index)) - if strongSelf.currentMaskPacksCollectionPosition != position { - strongSelf.currentMaskPacksCollectionPosition = position - strongSelf.maskItemCollectionsViewPosition.set(.single(position)) - } - } else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil { - var position: StickerPacksCollectionPosition? - if let bottomItem = bottomItem as? ChatMediaInputStickerGridItem { - position = clipScrollPosition(.scroll(aroundIndex: bottomItem.index)) - } - - if let position = position, strongSelf.currentMaskPacksCollectionPosition != position { - strongSelf.currentMaskPacksCollectionPosition = position - strongSelf.maskItemCollectionsViewPosition.set(.single(position)) - } - } - } - } - } - - self.currentStickerPacksCollectionPosition = .initial - self.stickerItemCollectionsViewPosition.set(.single(.initial)) - - self.currentMaskPacksCollectionPosition = .initial - self.maskItemCollectionsViewPosition.set(.single(.initial)) - - self.stickerPane.inputNodeInteraction = self.stickersNodeInteraction - self.maskPane.inputNodeInteraction = self.masksNodeInteraction - - paneDidScrollImpl = { [weak self] pane, state, transition in - self?.updatePaneDidScroll(pane: pane, state: state, transition: transition) - } - - selectStickerImpl = { [weak self] fileReference, node, rect in - return self?.selectSticker?(fileReference, node, rect) ?? false - } - - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let strongSelf = self { - strongSelf.setCurrentPane(index == 0 ? .stickers : .masks, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: nil) - } - } - } - - deinit { - self.disposable.dispose() - } - - override func didLoad() { - super.didLoad() - - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - self.panRecognizer = panRecognizer - self.view.addGestureRecognizer(panRecognizer) - } - - @objc private func cancelPressed() { - self.animateOut() - } - - private func setHighlightedStickerItemCollectionId(_ collectionId: ItemCollectionId) { - self.stickersNodeInteraction.highlightedStickerItemCollectionId = collectionId - if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .stickers { - self.stickersNodeInteraction.highlightedItemCollectionId = collectionId - } - var ensuredNodeVisible = false - var firstVisibleCollectionId: ItemCollectionId? - self.stickerListView.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { - if firstVisibleCollectionId == nil { - firstVisibleCollectionId = itemNode.currentCollectionId - } - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } - } - - if let currentView = self.currentStickerView, let firstVisibleCollectionId = firstVisibleCollectionId, !ensuredNodeVisible { - let targetIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == collectionId }) - let firstVisibleIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == firstVisibleCollectionId }) - if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { - let toRight = targetIndex > firstVisibleIndex - self.stickerListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) - } - } - } - - private func setHighlightedMaskItemCollectionId(_ collectionId: ItemCollectionId) { - self.masksNodeInteraction.highlightedStickerItemCollectionId = collectionId - if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .masks { - self.masksNodeInteraction.highlightedItemCollectionId = collectionId - } - var ensuredNodeVisible = false - var firstVisibleCollectionId: ItemCollectionId? - self.maskListView.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { - if firstVisibleCollectionId == nil { - firstVisibleCollectionId = itemNode.currentCollectionId - } - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { - itemNode.updateIsHighlighted() - if itemNode.currentCollectionId == collectionId { - self.stickerListView.ensureItemNodeVisible(itemNode) - ensuredNodeVisible = true - } - } - } - - if let currentView = self.currentMaskView, let firstVisibleCollectionId = firstVisibleCollectionId, !ensuredNodeVisible { - let targetIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == collectionId }) - let firstVisibleIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == firstVisibleCollectionId }) - if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { - let toRight = targetIndex > firstVisibleIndex - self.maskListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) - } - } - } - - private func setCurrentPane(_ pane: DrawingPaneType, transition: ContainedViewLayoutTransition, collectionIdHint: Int32? = nil) { - if let index = self.paneArrangement.panes.firstIndex(of: pane), index != self.paneArrangement.currentIndex, let layout = self.validLayout { - self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - - switch pane { - case .stickers: - if let highlightedStickerCollectionId = self.stickersNodeInteraction.highlightedStickerItemCollectionId { - self.setHighlightedStickerItemCollectionId(highlightedStickerCollectionId) - } else if let collectionIdHint = collectionIdHint { - self.setHighlightedStickerItemCollectionId(ItemCollectionId(namespace: collectionIdHint, id: 0)) - } - self.maskListView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: layout.size.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in - guard let strongSelf = self, completed else { - return - } - strongSelf.maskListView.isHidden = true - strongSelf.maskListView.layer.removeAllAnimations() - }) - self.stickerListView.layer.removeAllAnimations() - self.stickerListView.isHidden = false - self.stickerListView.layer.animatePosition(from: CGPoint(x: -layout.size.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - case .masks: - if let highlightedStickerCollectionId = self.stickersNodeInteraction.highlightedStickerItemCollectionId { - self.setHighlightedMaskItemCollectionId(highlightedStickerCollectionId) - } else if let collectionIdHint = collectionIdHint { - self.setHighlightedMaskItemCollectionId(ItemCollectionId(namespace: collectionIdHint, id: 0)) - } - self.stickerListView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -layout.size.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in - guard let strongSelf = self, completed else { - return - } - strongSelf.stickerListView.isHidden = true - strongSelf.stickerListView.layer.removeAllAnimations() - }) - self.maskListView.layer.removeAllAnimations() - self.maskListView.isHidden = false - self.maskListView.layer.animatePosition(from: CGPoint(x: layout.size.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - if let layout = self.validLayout { - self.updateLayout(layout, transition: transition) - } - } - - private func currentCollectionListPanelOffset() -> CGFloat { - let paneOffsets = self.paneArrangement.panes.map { pane -> CGFloat in - return self.stickerPane.collectionListPanelOffset - } - - let mainOffset = paneOffsets[self.paneArrangement.currentIndex] - if self.paneArrangement.indexTransition.isZero { - return mainOffset - } else { - var sideOffset: CGFloat? - if self.paneArrangement.indexTransition < 0.0 { - if self.paneArrangement.currentIndex != 0 { - sideOffset = paneOffsets[self.paneArrangement.currentIndex - 1] - } - } else { - if self.paneArrangement.currentIndex != paneOffsets.count - 1 { - sideOffset = paneOffsets[self.paneArrangement.currentIndex + 1] - } - } - if let sideOffset = sideOffset { - let interpolator = CGFloat.interpolator() - let value = interpolator(mainOffset, sideOffset, abs(self.paneArrangement.indexTransition)) as! CGFloat - return value - } else { - return mainOffset - } - } - } - - private func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - let insets = layout.insets(options: [.statusBar]) - let height = layout.size.height - - let _ = self.updateLayout(width: layout.size.width, topInset: insets.top, leftInset: insets.left, rightInset: insets.right, bottomInset: insets.bottom, standardInputHeight: height, inputHeight: height, maximumHeight: height, inputPanelHeight: height, transition: transition, deviceMetrics: layout.deviceMetrics, isVisible: true) - } - - func updateLayout(width: CGFloat, topInset: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { - let searchMode: ChatMediaInputSearchMode? = nil - - let displaySearch = !"".isEmpty //silence warning - let separatorHeight = max(UIScreenPixel, 1.0 - UIScreenPixel) - let topPanelHeight: CGFloat = 56.0 - let panelHeight: CGFloat - - let isExpanded: Bool = true - panelHeight = maximumHeight - - self.stickerPane.collectionListPanelOffset = 0.0 - - transition.updateFrame(node: self.topPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: topInset + topPanelHeight))) - - var cancelSize = self.cancelButton.measure(CGSize(width: width, height: .greatestFiniteMagnitude)) - cancelSize.width += 16.0 * 2.0 - transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: width - cancelSize.width, y: topInset + floorToScreenPixels((topPanelHeight - cancelSize.height) / 2.0)), size: cancelSize)) - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: width - cancelSize.width - 16.0 * 2.0), transition: transition) - transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: 16.0, y: topInset + floorToScreenPixels((topPanelHeight - controlSize.height) / 2.0)), size: controlSize)) - - if displaySearch { - if let searchContainerNode = self.searchContainerNode { - let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) - if searchContainerNode.supernode != nil { - transition.updateFrame(node: searchContainerNode, frame: containerFrame) - searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) - } else { - self.searchContainerNode = searchContainerNode - self.insertSubnode(searchContainerNode, belowSubnode: self.collectionListContainer) - searchContainerNode.frame = containerFrame - searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate) - var placeholderNode: PaneSearchBarPlaceholderNode? - let anchorTop = CGPoint(x: 0.0, y: 0.0) - let anchorTopView: UIView = self.view - if let searchMode = searchMode { - switch searchMode { - case .sticker: - self.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } - } - default: - break - } - } - - if let placeholderNode = placeholderNode { - searchContainerNode.animateIn(from: placeholderNode, anchorTop: anchorTop, anhorTopView: anchorTopView, transition: transition, completion: { - }) - } - } - } - } - - let bottomPanelHeight: CGFloat = 49.0 - let contentVerticalOffset: CGFloat = displaySearch ? -(inputPanelHeight + 41.0) : 0.0 - - let collectionListPanelOffset: CGFloat = 0.0 - - transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: maximumHeight))) - - transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: maximumHeight + contentVerticalOffset - bottomPanelHeight - bottomInset), size: CGSize(width: width, height: max(0.0, bottomPanelHeight + UIScreenPixel + bottomInset)))) - transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0), size: CGSize(width: width, height: bottomPanelHeight + bottomInset))) - - transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + topPanelHeight), size: CGSize(width: width, height: separatorHeight))) - transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: maximumHeight + contentVerticalOffset - bottomPanelHeight - bottomInset), size: CGSize(width: width, height: separatorHeight))) - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - - let listPosition = CGPoint(x: width / 2.0, y: (bottomPanelHeight - collectionListPanelOffset) / 2.0 + 15.0) - self.stickerListView.bounds = CGRect(x: 0.0, y: 0.0, width: bottomPanelHeight + 31.0, height: width) - transition.updatePosition(node: self.stickerListView, position: listPosition) - - self.maskListView.bounds = CGRect(x: 0.0, y: 0.0, width: bottomPanelHeight + 31.0, height: width) - transition.updatePosition(node: self.maskListView, position: listPosition) - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: bottomPanelHeight + 31.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: curve) - self.stickerListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - self.maskListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - var visiblePanes: [(DrawingPaneType, CGFloat)] = [] - - var paneIndex = 0 - for pane in self.paneArrangement.panes { - let paneOrigin = CGFloat(paneIndex - self.paneArrangement.currentIndex) * width - self.paneArrangement.indexTransition * width - if paneOrigin.isLess(than: width) && CGFloat(0.0).isLess(than: (paneOrigin + width)) { - visiblePanes.append((pane, paneOrigin)) - } - paneIndex += 1 - } - - var stickersVisible = false - var masksVisible = false - - for (pane, paneOrigin) in visiblePanes { - let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: topInset + topPanelHeight), size: CGSize(width: width - leftInset - rightInset, height: panelHeight - topInset - topPanelHeight - bottomInset - bottomPanelHeight)) - switch pane { - case .stickers: - if self.stickerPane.supernode == nil { - self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) - self.stickerPane.frame = CGRect(origin: CGPoint(x: -width, y: topInset + topPanelHeight), size: CGSize(width: width, height: panelHeight - topInset - topPanelHeight - bottomInset - bottomPanelHeight)) - } - if self.stickerListView.supernode == nil { - self.collectionListContainer.addSubnode(self.stickerListView) - } - if self.stickerPane.frame != paneFrame { - self.stickerPane.layer.removeAnimation(forKey: "position") - transition.updateFrame(node: self.stickerPane, frame: paneFrame) - } - stickersVisible = true - case .masks: - if self.maskPane.supernode == nil { - self.insertSubnode(self.maskPane, belowSubnode: self.collectionListContainer) - self.maskPane.frame = CGRect(origin: CGPoint(x: width, y: topInset + topPanelHeight), size: CGSize(width: width, height: panelHeight - topInset - topPanelHeight - bottomInset - bottomPanelHeight)) - } - if self.maskListView.supernode == nil { - self.collectionListContainer.addSubnode(self.maskListView) - } - if self.maskPane.frame != paneFrame { - self.maskPane.layer.removeAnimation(forKey: "position") - transition.updateFrame(node: self.maskPane, frame: paneFrame) - } - masksVisible = true - } - } - - self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - topInset - topPanelHeight - bottomInset - bottomPanelHeight), topInset: 0.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: stickersVisible, deviceMetrics: deviceMetrics, transition: transition) - - self.maskPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight - topInset - topPanelHeight - bottomInset - bottomPanelHeight), topInset: 0.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: masksVisible, deviceMetrics: deviceMetrics, transition: transition) - - if self.stickerPane.supernode != nil { - if !visiblePanes.contains(where: { $0.0 == .stickers }) { - if case .animated = transition { - if !self.animatingStickerPaneOut { - self.animatingStickerPaneOut = true - transition.animatePosition(node: self.stickerPane, to: CGPoint(x: -width + width / 2.0, y: self.stickerPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in - if let strongSelf = self, value { - strongSelf.animatingStickerPaneOut = false - strongSelf.stickerPane.removeFromSupernode() - } - }) - } - } else { - self.animatingStickerPaneOut = false - self.stickerPane.removeFromSupernode() - self.stickerListView.removeFromSupernode() - } - } - } else { - self.animatingStickerPaneOut = false - } - - if self.maskPane.supernode != nil { - if !visiblePanes.contains(where: { $0.0 == .masks }) { - if case .animated = transition { - if !self.animatingMaskPaneOut { - self.animatingMaskPaneOut = true - transition.animatePosition(node: self.maskPane, to: CGPoint(x: width + width / 2.0, y: self.maskPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in - if let strongSelf = self, value { - strongSelf.animatingMaskPaneOut = false - strongSelf.maskPane.removeFromSupernode() - } - }) - } - } else { - self.animatingMaskPaneOut = false - self.maskPane.removeFromSupernode() - self.maskListView.removeFromSupernode() - } - } - } else { - self.animatingMaskPaneOut = false - } - - if !displaySearch, let searchContainerNode = self.searchContainerNode { - self.searchContainerNode = nil - self.searchContainerNodeLoadedDisposable.set(nil) - - var paneIsEmpty = false - var placeholderNode: PaneSearchBarPlaceholderNode? - if let searchMode = searchMode { - switch searchMode { - case .sticker: - paneIsEmpty = true - self.stickerPane.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { - placeholderNode = itemNode - } - if let _ = itemNode as? ChatMediaInputStickerGridItemNode { - paneIsEmpty = false - } - } - default: - break - } - } - if let placeholderNode = placeholderNode { - searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in - searchContainerNode?.removeFromSupernode() - }) - } else { - searchContainerNode.removeFromSupernode() - } - } - -// if let panRecognizer = self.panRecognizer, panRecognizer.isEnabled != !displaySearch { -// panRecognizer.isEnabled = !displaySearch -// } -// - - return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) - } - - private func enqueuePanelTransition(listView: ListView, pane: ChatMediaInputStickerPane, transition: ChatMediaInputPanelTransition, firstTime: Bool, thenGridTransition gridTransition: ChatMediaInputGridTransition, gridFirstTime: Bool) { - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) - } else { - options.insert(.AnimateInsertion) - } - listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.enqueueGridTransition(pane: pane, transition: gridTransition, firstTime: gridFirstTime) - if !strongSelf.didSetReady && pane === strongSelf.stickerPane { - strongSelf.didSetReady = true - strongSelf._ready.set(.single(true)) - } - } - }) - } - - private func enqueueGridTransition(pane: ChatMediaInputStickerPane, transition: ChatMediaInputGridTransition, firstTime: Bool) { - var itemTransition: ContainedViewLayoutTransition = .immediate - if transition.animated { - itemTransition = .animated(duration: 0.3, curve: .spring) - } - pane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset, updateOpaqueState: transition.updateOpaqueState), completion: { _ in }) - } - - private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) { - pane.collectionListPanelOffset = 0.0 - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - self.stickerPane.layer.removeAllAnimations() - if self.animatingStickerPaneOut { - self.animatingStickerPaneOut = false - self.stickerPane.removeFromSupernode() - } - self.maskPane.layer.removeAllAnimations() - if self.animatingMaskPaneOut { - self.animatingMaskPaneOut = false - self.maskPane.removeFromSupernode() - } - case .changed: - if let layout = self.validLayout { - let translationX = -recognizer.translation(in: self.view).x - var indexTransition = translationX / layout.size.width - if self.paneArrangement.currentIndex == 0 { - indexTransition = max(0.0, indexTransition) - } else if self.paneArrangement.currentIndex == self.paneArrangement.panes.count - 1 { - indexTransition = min(0.0, indexTransition) - } - self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - self.updateLayout(layout, transition: .immediate) - } - case .ended: - if let layout = self.validLayout { - var updatedIndex = self.paneArrangement.currentIndex - if abs(self.paneArrangement.indexTransition * layout.size.width) > 30.0 { - if self.paneArrangement.indexTransition < 0.0 { - updatedIndex = max(0, self.paneArrangement.currentIndex - 1) - } else { - updatedIndex = min(self.paneArrangement.panes.count - 1, self.paneArrangement.currentIndex + 1) - } - } - self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) - self.segmentedControlNode.setSelectedIndex(updatedIndex, animated: true) - } - case .cancelled: - if let layout = self.validLayout { - self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - self.updateLayout(layout, transition: .animated(duration: 0.25, curve: .spring)) - } - default: - break - } - } - - fileprivate var didAppear: (() -> Void)? - fileprivate var willDisappear: (() -> Void)? - - func animateIn() { - self.isUserInteractionEnabled = true - self.isHidden = false - - if let hiddenPane = self.hiddenPane { - self.insertSubnode(hiddenPane, belowSubnode: self.collectionListContainer) - self.hiddenPane = nil - } - if let hiddenListView = self.hiddenListView { - self.collectionListContainer.addSubnode(hiddenListView) - self.hiddenListView = nil - } - - self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.didAppear?() - } - }) - } - - func animateOut() { - self.willDisappear?() - - self.isUserInteractionEnabled = false - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - if strongSelf.stickerPane.supernode != nil { - strongSelf.hiddenPane = strongSelf.stickerPane - strongSelf.hiddenListView = strongSelf.stickerListView - } else if strongSelf.maskPane.supernode != nil { - strongSelf.hiddenPane = strongSelf.maskPane - strongSelf.hiddenListView = strongSelf.maskListView - } - strongSelf.hiddenPane?.removeFromSupernode() - strongSelf.hiddenListView?.removeFromSupernode() - strongSelf.isHidden = true - } - }) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - self.updateLayout(layout, transition: transition) - } -} - -final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { - public var screenDidAppear: (() -> Void)? - public var screenWillDisappear: (() -> Void)? - - private let context: AccountContext - var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? - - private var controllerNode: DrawingStickersScreenNode { - return self.displayNode as! DrawingStickersScreenNode - } - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private var didPlayPresentationAnimation = false - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { - self.context = context - self.selectSticker = selectSticker - - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - super.init(navigationBarPresentationData: nil) - - self.navigationPresentation = .modal - - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previous = strongSelf.presentationData - strongSelf.presentationData = presentationData - - if previous.theme !== presentationData.theme || previous.strings !== presentationData.strings { - strongSelf.updatePresentationData() - } - } - }) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - private func updatePresentationData() { - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - } - - override public func loadDisplayNode() { - self.displayNode = DrawingStickersScreenNode( - context: self.context, - selectSticker: { [weak self] file, sourceView, sourceRect in - if let strongSelf = self, let selectSticker = strongSelf.selectSticker { - (strongSelf.displayNode as! DrawingStickersScreenNode).animateOut() - return selectSticker(file, sourceView, sourceRect) - } else { - return false - } - } - ) - (self.displayNode as! DrawingStickersScreenNode).dismiss = { [weak self] in - self?.dismiss() - } - (self.displayNode as! DrawingStickersScreenNode).didAppear = { [weak self] in - self?.screenDidAppear?() - } - (self.displayNode as! DrawingStickersScreenNode).willDisappear = { [weak self] in - self?.screenWillDisappear?() - } - self._ready.set(self.controllerNode.ready.get()) - - super.displayNodeDidLoad() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - (self.displayNode as! DrawingStickersScreenNode).animateIn() - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationHeight: 0.0, transition: transition) - } - - func restore() { - (self.displayNode as! DrawingStickersScreenNode).animateIn() - } - - func invalidate() { - self.dismiss() - } -} diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 9c53911751b..0c7657eecad 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -232,18 +232,20 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } self.previousMediaReference = updatedMediaReference + let hasSpoiler = message?.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) ?? false + var isPhoto = false var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: self.context.account, photoReference: imageReference) + updateImageSignal = chatMessagePhotoThumbnail(account: self.context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference, blurred: hasSpoiler) isPhoto = true } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: self.context.account, fileReference: fileReference) + updateImageSignal = chatMessageVideoThumbnail(account: self.context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: self.context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + updateImageSignal = chatWebpageSnippetFile(account: self.context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } else { diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index 782187ba89c..5793ab30965 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -13,6 +13,7 @@ import ChatPresentationInterfaceState import AnimationCache import MultiAnimationRenderer import TextFormat +import ChatControllerInteraction private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { case symbol(String) @@ -206,7 +207,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { if let file = file { loop: for attribute in file.attributes { switch attribute { - case let .CustomEmoji(_, displayText, _): + case let .CustomEmoji(_, _, displayText, _): text = displayText emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop diff --git a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift index 6420e83f3bf..842d3266d10 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift @@ -135,6 +135,7 @@ final class EmojisChatInputPanelItemNode: ListViewItemNode { } else { emojiView = EmojiTextAttachmentView( context: item.context, + userLocation: .other, emoji: ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, fileId: file.fileId.id, diff --git a/submodules/TelegramUI/Sources/GridMessageItem.swift b/submodules/TelegramUI/Sources/GridMessageItem.swift index b5aa53490bf..49f03cbdd5c 100644 --- a/submodules/TelegramUI/Sources/GridMessageItem.swift +++ b/submodules/TelegramUI/Sources/GridMessageItem.swift @@ -14,6 +14,7 @@ import PhotoResources import GridMessageSelectionNode import ContextUI import ChatMessageInteractiveMediaBadge +import ChatControllerInteraction private func mediaForMessage(_ message: Message) -> Media? { for media in message.media { @@ -219,7 +220,7 @@ final class GridMessageItemNode: GridItemNode { if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize.cgSize - self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) + self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) self.fetchStatusDisposable.set(nil) self.statusNode.transitionToState(.none, completion: { [weak self] in @@ -229,7 +230,7 @@ final class GridMessageItemNode: GridItemNode { self.resourceStatus = nil } else if let file = media as? TelegramMediaFile, file.isVideo { mediaDimensions = file.dimensions?.cgSize - self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) self.mediaBadgeNode.isHidden = false diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index 97ef0ffca0f..b1b70cb4026 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -11,6 +11,7 @@ import AccountContext import AccountContext import ItemListUI import ChatPresentationInterfaceState +import ChatControllerInteraction private struct HashtagChatInputContextPanelEntryStableId: Hashable { let text: String diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index e716cad5c0d..0348c7b5fa5 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -15,6 +15,7 @@ import ContextUI import ChatPresentationInterfaceState import UndoUI import PremiumUI +import ChatControllerInteraction private struct ChatContextResultStableId: Hashable { let result: ChatContextResult diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 4cf85fe0fc9..f86022e58a7 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -15,6 +15,7 @@ import TelegramPresentationData import AccountContext import ShimmerEffect import SoftwareVideo +import MultiplexedVideoNode final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { let account: Account @@ -350,11 +351,11 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if updatedImageResource { if let imageResource = imageResource { if let stickerFile = stickerFile { - updateImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) + updateImageSignal = chatMessageSticker(account: item.account, userLocation: .other, file: stickerFile, small: false, fetched: true) } else { - let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage), synchronousLoad: true) + updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), synchronousLoad: true) } } else { updateImageSignal = .complete() @@ -398,7 +399,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: item.account, fileReference: .standalone(media: videoFile), layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder) strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { @@ -436,7 +437,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } let dimensions = animatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) - strongSelf.fetchDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(animatedStickerFile), resource: animatedStickerFile.resource).start()) + strongSelf.fetchDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, userLocation: .other, fileReference: stickerPackFileReference(animatedStickerFile), resource: animatedStickerFile.resource).start()) animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: animatedStickerFile.resource, isVideo: animatedStickerFile.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) } } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index a19783d16de..82a28606dd1 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -161,9 +161,9 @@ final class HorizontalStickerGridItemNode: GridItemNode { let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) if item.file.isVideoSticker { - self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, file: item.file, small: true, synchronousLoad: false)) + self.imageNode.setSignal(chatMessageSticker(postbox: account.postbox, userLocation: .other, file: item.file, small: true, synchronousLoad: false)) } else { - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false)) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, userLocation: .other, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false)) } animationNode.started = { [weak self] in guard let strongSelf = self else { @@ -183,17 +183,17 @@ final class HorizontalStickerGridItemNode: GridItemNode { } animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource, isVideo: item.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) } else { self.imageNode.alpha = 1.0 - self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true)) + self.imageNode.setSignal(chatMessageSticker(account: account, userLocation: .other, file: item.file, small: true)) if let currentAnimationNode = self.animationNode { self.animationNode = nil currentAnimationNode.removeFromSupernode() } - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start()) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, userLocation: .other, fileReference: stickerPackFileReference(item.file), resource: chatMessageStickerResource(file: item.file, small: true)).start()) } if item.file.isPremiumSticker { diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 828551c40f5..952a9b0ea23 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -15,6 +15,7 @@ import ContextUI import ChatPresentationInterfaceState import PremiumUI import UndoUI +import ChatControllerInteraction final class HorizontalStickersChatContextPanelInteraction { var previewedStickerItem: TelegramMediaFile? diff --git a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift index cc11c67273f..5c4123e5fc3 100644 --- a/submodules/TelegramUI/Sources/InChatPrefetchManager.swift +++ b/submodules/TelegramUI/Sources/InChatPrefetchManager.swift @@ -115,7 +115,7 @@ final class InChatPrefetchManager { } } else if case .prefetch = automaticDownload, message.id.peerId.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let _ = file.size { - context.fetchDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), duration: 4.0).start()) + context.fetchDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), duration: 4.0).start()) } } } diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 193604a5014..e999aaea281 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -14,6 +14,7 @@ import ContextUI import ChatPresentationInterfaceState import PremiumUI import UndoUI +import ChatControllerInteraction private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate { private final class DisplayItem { diff --git a/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift b/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift index b76a421208e..cc385e62821 100644 --- a/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift +++ b/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift @@ -92,8 +92,8 @@ private final class LargeEmojiActionSheetItemNode: ActionSheetItemNode { self.accessibilityArea.accessibilityTraits = .staticText let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: true), attemptSynchronously: true) - self.disposable.set(freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)), fitzModifier: fitzModifier, thumbnail: false, synchronousLoad: true), attemptSynchronously: true) + self.disposable.set(freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start()) self.setupTimestamp = CACurrentMediaTime() diff --git a/submodules/TelegramUI/Sources/LegacyCamera.swift b/submodules/TelegramUI/Sources/LegacyCamera.swift index 479986fc68b..23c6bf543b3 100644 --- a/submodules/TelegramUI/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -70,13 +70,6 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } controller.stickersContext = paintStickersContext controller.isImportant = true diff --git a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift index 5872b878d89..5c8c0452b15 100644 --- a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift @@ -180,7 +180,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } diff --git a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift index 44b58d9a484..5589c14e15a 100644 --- a/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift @@ -37,7 +37,7 @@ private func animationItem(account: Account, emojis: Signal<[TelegramMediaFile], let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) - let fetched = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: file)) + let fetched = freeMediaFileInteractiveFetched(account: account, userLocation: .other, fileReference: .standalone(media: file)) let animationItem = Signal { subscriber in let fetchedDisposable = fetched.start() let resourceDisposable = (chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false) diff --git a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift index e7554c8de79..bdb54b03f44 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift @@ -15,6 +15,7 @@ import AccountContext import LocalizedPeerData import ItemListUI import ChatPresentationInterfaceState +import ChatControllerInteraction private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { let index: Int diff --git a/submodules/TelegramUI/Sources/MultiScaleTextNode.swift b/submodules/TelegramUI/Sources/MultiScaleTextNode.swift index 3c5a42fe2f7..37548d5d821 100644 --- a/submodules/TelegramUI/Sources/MultiScaleTextNode.swift +++ b/submodules/TelegramUI/Sources/MultiScaleTextNode.swift @@ -19,11 +19,16 @@ private final class MultiScaleTextStateNode: ASDisplayNode { } final class MultiScaleTextState { - let attributedText: NSAttributedString + struct Attributes { + var font: UIFont + var color: UIColor + } + + let attributes: Attributes let constrainedSize: CGSize - init(attributedText: NSAttributedString, constrainedSize: CGSize) { - self.attributedText = attributedText + init(attributes: Attributes, constrainedSize: CGSize) { + self.attributes = attributes self.constrainedSize = constrainedSize } } @@ -49,7 +54,7 @@ final class MultiScaleTextNode: ASDisplayNode { return self.stateNodes[key]?.textNode } - func updateLayout(states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] { + func updateLayout(text: String, states: [AnyHashable: MultiScaleTextState], mainState: AnyHashable) -> [AnyHashable: MultiScaleTextLayout] { assert(Set(states.keys) == Set(self.stateNodes.keys)) assert(states[mainState] != nil) @@ -57,7 +62,7 @@ final class MultiScaleTextNode: ASDisplayNode { var mainLayout: MultiScaleTextLayout? for (key, state) in states { if let node = self.stateNodes[key] { - node.textNode.attributedText = state.attributedText + node.textNode.attributedText = NSAttributedString(string: text, font: state.attributes.font, textColor: state.attributes.color) let nodeSize = node.textNode.updateLayout(state.constrainedSize) let nodeLayout = MultiScaleTextLayout(size: nodeSize) if key == mainState { diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index d35076e91e0..850cf2e4df8 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -240,10 +240,10 @@ public final class NotificationViewControllerImpl { return } if let imageReference = accountAndImage.1 { - strongSelf.imageNode.setSignal(chatMessagePhoto(postbox: accountAndImage.0.postbox, photoReference: imageReference)) + strongSelf.imageNode.setSignal(chatMessagePhoto(postbox: accountAndImage.0.postbox, userLocation: .other, photoReference: imageReference)) accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) - strongSelf.fetchedDisposable.set(standaloneChatMessagePhotoInteractiveFetched(account: accountAndImage.0, photoReference: imageReference).start()) + strongSelf.fetchedDisposable.set(standaloneChatMessagePhotoInteractiveFetched(account: accountAndImage.0, userLocation: .other, photoReference: imageReference).start()) } })) } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { @@ -313,15 +313,15 @@ public final class NotificationViewControllerImpl { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 512.0, height: 512.0)) if file.isVideoSticker { - strongSelf.imageNode.setSignal(chatMessageSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false)) + strongSelf.imageNode.setSignal(chatMessageSticker(postbox: accountAndImage.0.postbox, userLocation: .other, file: fileReference.media, small: false)) } else { - strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false, size: fittedDimensions)) + strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, userLocation: .other, file: fileReference.media, small: false, size: fittedDimensions)) } animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: accountAndImage.0, resource: fileReference.media.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) animatedStickerNode.visibility = true accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) - strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, fileReference: fileReference).start()) + strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, userLocation: .other, fileReference: fileReference).start()) } else if file.isSticker { if let animatedStickerNode = strongSelf.animatedStickerNode { animatedStickerNode.removeFromSupernode() @@ -329,10 +329,10 @@ public final class NotificationViewControllerImpl { } strongSelf.imageNode.isHidden = false - strongSelf.imageNode.setSignal(chatMessageSticker(account: accountAndImage.0, file: file, small: false)) + strongSelf.imageNode.setSignal(chatMessageSticker(account: accountAndImage.0, userLocation: .other, file: file, small: false)) accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) - strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, fileReference: fileReference).start()) + strongSelf.fetchedDisposable.set(freeMediaFileInteractiveFetched(account: accountAndImage.0, userLocation: .other, fileReference: fileReference).start()) } } })) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index ba65a7fb87f..0d17a3d37db 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -260,7 +260,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) { if let (webpage, anchor) = instantPageAndAnchor(message: message) { - let pageController = InstantPageController(context: context, webPage: webpage, sourcePeerType: sourcePeerType ?? .channel, anchor: anchor) + let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) + + let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) navigationController.pushViewController(pageController) } } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 13b52b291bc..3f89bac5054 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -72,7 +72,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur case let .botStart(peer, payload): openPeer(EnginePeer(peer), .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .groupBotStart(botPeerId, payload, adminRights): - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title)) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title, selectForumThreads: true)) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id @@ -204,7 +204,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur }) present(controller, nil) case let .instantView(webpage, anchor): - navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .channel, anchor: anchor)) + navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor)) case let .join(link): dismissInput() present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in @@ -322,7 +322,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur present(shareController, nil) context.sharedContext.applicationBindings.dismissNativeController() } else { - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true)) controller.peerSelected = { [weak controller] peer, _ in let peerId = peer.id @@ -376,7 +376,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur subscriber.putNext((nil, settings, themeInfo)) subscriber.putCompletion() } else if let resource = themeInfo.file?.resource { - disposables.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: resource)).start()) + disposables.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: resource)).start()) let maybeFetched = context.sharedContext.accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) |> mapToSignal { maybeData -> Signal in @@ -604,7 +604,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } if let navigationController = navigationController { - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat)) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat, selectForumThreads: true)) controller.peerSelected = { [weak navigationController] peer, _ in guard let navigationController else { return @@ -632,8 +632,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) let botPeer = EnginePeer(bot.peer) - let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, completion: { - let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId) + let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError) }, completed: { @@ -656,7 +656,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } if let navigationController = navigationController { - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat)) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, filter: filters, hasChatListSelector: true, hasContactSelector: false, title: presentationData.strings.WebApp_SelectChat, selectForumThreads: true)) controller.peerSelected = { [weak navigationController] peer, _ in guard let navigationController else { return diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2f4c04b354b..dc5728da273 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import AccountContext import DirectionalPanGesture import ChatPresentationInterfaceState +import ChatControllerInteraction final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestureRecognizerDelegate { let ready = Promise() diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 0e52bef025d..7376f7efbe7 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -106,6 +106,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private let scrubberNode: MediaPlayerScrubbingNode private let leftDurationLabel: MediaPlayerTimeTextNode private let rightDurationLabel: MediaPlayerTimeTextNode + private let infoNode: ASTextNode private let backwardButton: IconButtonNode private let forwardButton: IconButtonNode @@ -149,6 +150,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { private var scrubbingDisposable: Disposable? private var leftDurationLabelPushed = false private var rightDurationLabelPushed = false + private var infoNodePushed = false private var currentDuration: Double = 0.0 private var currentPosition: Double = 0.0 @@ -196,6 +198,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.rightDurationLabel.alignment = .right self.rightDurationLabel.keepPreviousValueOnEmptyState = true + self.infoNode = ASTextNode() + self.infoNode.maximumNumberOfLines = 1 + self.infoNode.isUserInteractionEnabled = false + self.infoNode.displaysAsynchronously = false + self.rateButton = HighlightableButtonNode() self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0) self.rateButton.displaysAsynchronously = false @@ -238,6 +245,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.addSubnode(self.leftDurationLabel) self.addSubnode(self.rightDurationLabel) + self.addSubnode(self.infoNode) self.addSubnode(self.rateButton) self.addSubnode(self.scrubberNode) @@ -283,16 +291,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode { } let leftDurationLabelPushed: Bool let rightDurationLabelPushed: Bool + let infoNodePushed: Bool if let value = value { leftDurationLabelPushed = value < 0.16 rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74) + infoNodePushed = value >= 0.16 && value <= 0.84 } else { leftDurationLabelPushed = false rightDurationLabelPushed = false + infoNodePushed = false } - if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed { + if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed { strongSelf.leftDurationLabelPushed = leftDurationLabelPushed strongSelf.rightDurationLabelPushed = rightDurationLabelPushed + strongSelf.infoNodePushed = infoNodePushed if let layout = strongSelf.validLayout { let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring)) @@ -778,6 +790,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0 transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0))) + let infoLabelVerticalOffset: CGFloat = self.infoNodePushed ? 6.0 : 0.0 + + let infoSize = self.infoNode.measure(CGSize(width: width - 60.0 * 2.0 - 100.0, height: 100.0)) + self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize) + transition.updatePosition(node: self.infoNode, position: CGPoint(x: width / 2.0, y: scrubberVerticalOrigin + 14.0 + infoLabelVerticalOffset + infoSize.height / 2.0)) + + let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0))) diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift index c8e69688b80..c129a42b4c5 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenActionItem.swift @@ -1,6 +1,8 @@ import AsyncDisplayKit import Display +import SwiftSignalKit import TelegramPresentationData +import AvatarNode enum PeerInfoScreenActionColor { case accent @@ -18,14 +20,16 @@ final class PeerInfoScreenActionItem: PeerInfoScreenItem { let text: String let color: PeerInfoScreenActionColor let icon: UIImage? + let iconSignal: Signal? let alignment: PeerInfoScreenActionAligmnent let action: (() -> Void)? - init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, icon: UIImage? = nil, alignment: PeerInfoScreenActionAligmnent = .natural, action: (() -> Void)?) { + init(id: AnyHashable, text: String, color: PeerInfoScreenActionColor = .accent, icon: UIImage? = nil, iconSignal: Signal? = nil, alignment: PeerInfoScreenActionAligmnent = .natural, action: (() -> Void)?) { self.id = id self.text = text self.color = color self.icon = icon + self.iconSignal = iconSignal self.alignment = alignment self.action = action } @@ -45,6 +49,8 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { private var item: PeerInfoScreenActionItem? + private let iconDisposable = MetaDisposable() + override init() { var bringToFrontForHighlightImpl: (() -> Void)? self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) @@ -79,17 +85,21 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.addSubnode(self.activateArea) } + deinit { + self.iconDisposable.dispose() + } + override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenActionItem else { return 10.0 } self.item = item - + self.selectionNode.pressed = item.action let sideInset: CGFloat = 16.0 + safeInsets.left - var leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0) + var leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0) var iconInset = sideInset if case .peerList = item.alignment { leftInset += 5.0 @@ -126,6 +136,19 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.iconNode.image = generateTintedImage(image: icon, color: textColorValue) let iconFrame = CGRect(origin: CGPoint(x: iconInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size) transition.updateFrame(node: self.iconNode, frame: iconFrame) + } else if let iconSignal = item.iconSignal { + self.iconDisposable.set((iconSignal + |> deliverOnMainQueue).start(next: { [weak self] image in + if let strongSelf = self, let image { + strongSelf.iconNode.image = image + let iconFrame = CGRect(origin: CGPoint(x: iconInset, y: floorToScreenPixels((height - image.size.height) / 2.0)), size: image.size) + transition.updateFrame(node: strongSelf.iconNode, frame: iconFrame) + } + })) + if self.iconNode.supernode == nil { + self.addSubnode(self.iconNode) + } + } else if self.iconNode.supernode != nil { self.iconNode.image = nil self.iconNode.removeFromSupernode() diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index f3c856d0e8d..98586b44c1c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -11,6 +11,7 @@ import TelegramUIPreferences import ItemListPeerItem import MergeLists import ItemListUI +import ChatControllerInteraction private struct GroupsInCommonListTransaction { let deletions: [ListViewDeleteItem] diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift index 5b9fb9fef79..a44175621a2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -15,6 +15,7 @@ import OverlayStatusController import ListMessageItem import UndoUI import ChatPresentationInterfaceState +import ChatControllerInteraction final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 100d99c963f..0ff7cd6a653 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -23,6 +23,8 @@ import TelegramNotices import TelegramUIPreferences import CheckNode import AppBundle +import ChatControllerInteraction +import InvisibleInkDustNode private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext @@ -43,6 +45,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { init( context: AccountContext, + userLocation: MediaResourceUserLocation, file: FileMediaReference ) { self.context = context @@ -71,6 +74,8 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, + userLocation: userLocation, + userContentType: .other, fileReference: self.file, automaticallyFetchHeader: true ) @@ -772,9 +777,11 @@ private protocol ItemLayer: SparseItemGridLayer { var disposable: Disposable? { get set } var hasContents: Bool { get set } + func setSpoilerContents(_ contents: Any?) func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) + func updateHasSpoiler(hasSpoiler: Bool) func bind(item: VisualMediaItem) func unbind() @@ -785,6 +792,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { var durationLayer: DurationLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? + var dustLayer: MediaDustLayer? var disposable: Disposable? var hasContents: Bool = false @@ -812,6 +820,12 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.contents = image.cgImage } } + + func setSpoilerContents(_ contents: Any?) { + if let image = contents as? UIImage { + self.dustLayer?.contents = image.cgImage + } + } override func action(forKey event: String) -> CAAction? { return nullAction @@ -869,6 +883,24 @@ private final class GenericItemLayer: CALayer, ItemLayer { } } } + + func updateHasSpoiler(hasSpoiler: Bool) { + if hasSpoiler { + if let _ = self.dustLayer { + } else { + let dustLayer = MediaDustLayer() + self.dustLayer = dustLayer + self.addSublayer(dustLayer) + if !self.bounds.isEmpty { + dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + dustLayer.updateLayout(size: self.bounds.size) + } + } + } else if let dustLayer = self.dustLayer { + self.dustLayer = nil + dustLayer.removeFromSuperlayer() + } + } func unbind() { self.item = nil @@ -890,6 +922,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL var durationLayer: DurationLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? + var dustLayer: MediaDustLayer? var disposable: Disposable? var hasContents: Bool = false @@ -931,6 +964,12 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } } } + + func setSpoilerContents(_ contents: Any?) { + if let image = contents as? UIImage { + self.dustLayer?.contents = image.cgImage + } + } func bind(item: VisualMediaItem) { self.item = item @@ -984,6 +1023,24 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } } } + + func updateHasSpoiler(hasSpoiler: Bool) { + if hasSpoiler { + if let _ = self.dustLayer { + } else { + let dustLayer = MediaDustLayer() + self.dustLayer = dustLayer + self.addSublayer(dustLayer) + if !self.bounds.isEmpty { + dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + dustLayer.updateLayout(size: self.bounds.size) + } + } + } else if let dustLayer = self.dustLayer { + self.dustLayer = nil + dustLayer.removeFromSuperlayer() + } + } func unbind() { self.item = nil @@ -1190,6 +1247,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? + + var revealedSpoilerMessageIds = Set() private var shimmerImages: [CGFloat: UIImage] = [:] @@ -1391,7 +1450,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } let message = item.message - + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id) + layer.updateHasSpoiler(hasSpoiler: hasSpoiler) + var selectedMedia: Media? for media in message.media { if let image = media as? TelegramMediaImage { @@ -1404,7 +1465,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } if let selectedMedia = selectedMedia { - if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, synchronous: synchronous == .full) { + if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler, synchronous: synchronous == .full) { if let image = result.image { layer.setContents(image) switch synchronous { @@ -1419,6 +1480,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme layer.hasContents = true } } + if let image = result.blurredImage { + layer.setSpoilerContents(image) + } if let loadSignal = result.loadSignal { layer.disposable?.dispose() let startTimestamp = CFAbsoluteTimeGetCurrent() @@ -1490,7 +1554,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } else { layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false) } - + layer.bind(item: item) } } @@ -1660,7 +1724,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, contentType: ContentType, captureProtected: Bool) { self.context = context self.peerId = peerId @@ -2312,6 +2376,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro if let item = itemLayer.item { if self.itemInteraction.hiddenMedia[item.message.id] != nil { itemLayer.isHidden = true + itemLayer.updateHasSpoiler(hasSpoiler: false) + self.itemGridBinding.revealedSpoilerMessageIds.insert(item.message.id) } else { itemLayer.isHidden = false } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 7d8554d4653..18676b8e99a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -400,7 +400,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if peer.isDeleted { overrideImage = .deletedIcon } else if let previousItem = previousItem, item == nil { - if case let .image(_, representations, _, _) = previousItem, let rep = representations.last { + if case let .image(_, representations, _, _, _) = previousItem, let rep = representations.last { self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) } overrideImage = AvatarNodeImageOverride.none @@ -500,7 +500,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { videoId = videoId &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail @@ -515,7 +515,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() @@ -669,7 +669,14 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { clipStyle = .round } - if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { + var isPersonal = false + if let updatingAvatar, case let .image(image) = updatingAvatar, image.isPersonal { + isPersonal = true + } + + if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + || isPersonal + || self.currentRepresentation != nil && updatingAvatar == nil { var overlayHidden = true if let updatingAvatar = updatingAvatar { overlayHidden = false @@ -778,7 +785,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { if canEdit, peer.profileImageRepresentations.isEmpty { overrideImage = .editAvatarIcon(forceNone: true) } else if let previousItem = previousItem, item == nil { - if case let .image(_, representations, _, _) = previousItem, let rep = representations.last { + if case let .image(_, representations, _, _, _) = previousItem, let rep = representations.last { self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation) } overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none @@ -825,7 +832,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail): + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail @@ -838,7 +845,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) + let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() @@ -913,7 +920,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) self.listContainerTransformNode = ASDisplayNode() - self.listContainerNode = PeerInfoAvatarListContainerNode(context: context) + self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings) self.listContainerNode.clipsToBounds = true self.listContainerNode.isHidden = true @@ -2499,7 +2506,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { strongSelf.emojiStatusFileAndPackTitle.set(.never()) for attribute in emojiFile.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute, let packReference = packReference { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { strongSelf.emojiStatusPackDisposable.set((strongSelf.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) |> filter { result in if case .result = result { @@ -2639,14 +2646,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { var isPremium = false var isVerified = false var isFake = false - let smallTitleString: NSAttributedString - let titleString: NSAttributedString - let smallSubtitleString: NSAttributedString - let subtitleString: NSAttributedString + let titleStringText: String + let smallTitleAttributes: MultiScaleTextState.Attributes + let titleAttributes: MultiScaleTextState.Attributes + let subtitleStringText: String + let smallSubtitleAttributes: MultiScaleTextState.Attributes + let subtitleAttributes: MultiScaleTextState.Attributes var subtitleIsButton: Bool = false - var panelSubtitleString: NSAttributedString? - var nextPanelSubtitleString: NSAttributedString? - let usernameString: NSAttributedString + var panelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? + var nextPanelSubtitleString: (text: String, attributes: MultiScaleTextState.Attributes)? + let usernameString: (text: String, attributes: MultiScaleTextState.Attributes) if let peer = peer { isPremium = peer.isPremium isVerified = peer.isVerified @@ -2676,8 +2685,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - titleString = NSAttributedString(string: title, font: Font.regular(30.0), textColor: presentationData.theme.list.itemPrimaryTextColor) - smallTitleString = NSAttributedString(string: title, font: Font.regular(30.0), textColor: .white) + titleStringText = title + titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: presentationData.theme.list.itemPrimaryTextColor) + smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white) + if self.isSettings, let user = peer as? TelegramUser { // MARK: Nicegram Hide phone var formattedPhone = formatPhoneNumber(context: self.context, number: user.phone ?? "") @@ -2694,9 +2705,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitle = "@\(mainUsername)" } } - smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) - subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + subtitleStringText = subtitle + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } else if let _ = threadData { let subtitleColor: UIColor subtitleColor = presentationData.theme.list.itemAccentColor @@ -2704,9 +2717,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { let statusText: String statusText = peer.debugDisplayTitle - smallSubtitleString = NSAttributedString(string: statusText, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) - subtitleString = NSAttributedString(string: statusText, font: Font.semibold(15.0), textColor: subtitleColor) - usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + subtitleStringText = statusText + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.semibold(15.0), color: subtitleColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) subtitleIsButton = true @@ -2718,10 +2733,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } - panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(17.0), textColor: subtitleColor) + panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = NSAttributedString(string: nextPanelStatusData.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else if let statusData = statusData { let subtitleColor: UIColor @@ -2730,9 +2745,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } - smallSubtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7)) - subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(17.0), textColor: subtitleColor) - usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + + subtitleStringText = statusData.text + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: UIColor(white: 1.0, alpha: 0.7)) + + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) let (maybePanelStatusData, maybeNextPanelStatusData, _) = panelStatusData if let panelStatusData = maybePanelStatusData { @@ -2742,22 +2760,28 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else { subtitleColor = presentationData.theme.list.itemSecondaryTextColor } - panelSubtitleString = NSAttributedString(string: panelStatusData.text, font: Font.regular(17.0), textColor: subtitleColor) + panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } if let nextPanelStatusData = maybeNextPanelStatusData { - nextPanelSubtitleString = NSAttributedString(string: nextPanelStatusData.text, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + nextPanelSubtitleString = (nextPanelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else { - subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - smallSubtitleString = subtitleString - usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + subtitleStringText = " " + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } } else { - titleString = NSAttributedString(string: " ", font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) - smallTitleString = titleString - subtitleString = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - smallSubtitleString = subtitleString - usernameString = NSAttributedString(string: "", font: Font.regular(15.0), textColor: presentationData.theme.list.itemSecondaryTextColor) + titleStringText = " " + titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: presentationData.theme.list.itemPrimaryTextColor) + smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(24.0), color: .white) + + subtitleStringText = " " + subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor) + + usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(15.0), color: presentationData.theme.list.itemSecondaryTextColor)) } let textSideInset: CGFloat = 36.0 @@ -2765,17 +2789,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) - let titleNodeLayout = self.titleNode.updateLayout(states: [ - TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize), - TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallTitleString, constrainedSize: titleConstrainedSize) + let titleNodeLayout = self.titleNode.updateLayout(text: titleStringText, states: [ + TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributes: smallTitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) - self.titleNode.accessibilityLabel = titleString.string + self.titleNode.accessibilityLabel = titleStringText - let subtitleNodeLayout = self.subtitleNode.updateLayout(states: [ - TitleNodeStateRegular: MultiScaleTextState(attributedText: subtitleString, constrainedSize: titleConstrainedSize), - TitleNodeStateExpanded: MultiScaleTextState(attributedText: smallSubtitleString, constrainedSize: titleConstrainedSize) + let subtitleNodeLayout = self.subtitleNode.updateLayout(text: subtitleStringText, states: [ + TitleNodeStateRegular: MultiScaleTextState(attributes: subtitleAttributes, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributes: smallSubtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) - self.subtitleNode.accessibilityLabel = subtitleString.string + self.subtitleNode.accessibilityLabel = subtitleStringText if subtitleIsButton { let subtitleBackgroundNode: ASDisplayNode @@ -2868,25 +2892,25 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } - let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(states: [ - TitleNodeStateRegular: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize), - TitleNodeStateExpanded: MultiScaleTextState(attributedText: panelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize) + let panelSubtitleNodeLayout = self.panelSubtitleNode.updateLayout(text: panelSubtitleString?.text ?? subtitleStringText, states: [ + TitleNodeStateRegular: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributes: panelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) - self.panelSubtitleNode.accessibilityLabel = (panelSubtitleString ?? subtitleString).string + self.panelSubtitleNode.accessibilityLabel = panelSubtitleString?.text ?? subtitleStringText - let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(states: [ - TitleNodeStateRegular: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize), - TitleNodeStateExpanded: MultiScaleTextState(attributedText: nextPanelSubtitleString ?? subtitleString, constrainedSize: titleConstrainedSize) + let nextPanelSubtitleNodeLayout = self.nextPanelSubtitleNode.updateLayout(text: nextPanelSubtitleString?.text ?? subtitleStringText, states: [ + TitleNodeStateRegular: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize), + TitleNodeStateExpanded: MultiScaleTextState(attributes: nextPanelSubtitleString?.attributes ?? subtitleAttributes, constrainedSize: titleConstrainedSize) ], mainState: TitleNodeStateRegular) if let _ = nextPanelSubtitleString { self.nextPanelSubtitleNode.isHidden = false } - let usernameNodeLayout = self.usernameNode.updateLayout(states: [ - TitleNodeStateRegular: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), - TitleNodeStateExpanded: MultiScaleTextState(attributedText: usernameString, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) + let usernameNodeLayout = self.usernameNode.updateLayout(text: usernameString.text, states: [ + TitleNodeStateRegular: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: titleConstrainedSize.width, height: titleConstrainedSize.height)), + TitleNodeStateExpanded: MultiScaleTextState(attributes: usernameString.attributes, constrainedSize: CGSize(width: width - titleNodeLayout[TitleNodeStateExpanded]!.size.width - 8.0, height: titleConstrainedSize.height)) ], mainState: TitleNodeStateRegular) - self.usernameNode.accessibilityLabel = usernameString.string + self.usernameNode.accessibilityLabel = usernameString.text let avatarCenter: CGPoint if let transitionSourceAvatarFrame = transitionSourceAvatarFrame { @@ -2992,7 +3016,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { subtitleAlpha = 1.0 - titleCollapseFraction panelSubtitleAlpha = 0.0 } else { - if (panelSubtitleString ?? subtitleString).string != subtitleString.string { + if (panelSubtitleString?.text ?? subtitleStringText) != subtitleStringText { subtitleAlpha = 1.0 - effectiveAreaExpansionFraction panelSubtitleAlpha = effectiveAreaExpansionFraction subtitleOffset = -effectiveAreaExpansionFraction * 5.0 @@ -3421,6 +3445,11 @@ final class PeerInfoHeaderNode: ASDisplayNode { return nil } + let setByFrame = self.avatarListNode.listContainerNode.setByYouNode.view.convert(self.avatarListNode.listContainerNode.setByYouNode.bounds, to: self.view).insetBy(dx: -44.0, dy: 0.0) + if self.avatarListNode.listContainerNode.setByYouNode.alpha > 0.0, setByFrame.contains(point) { + return self.avatarListNode.listContainerNode.setByYouNode.view + } + if !(self.state?.isEditing ?? false) { switch self.currentCredibilityIcon { case .premium, .emojiStatus: diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift index 831fb5442cf..df3d35aca63 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoMembers.swift @@ -131,6 +131,7 @@ private final class PeerInfoMembersContextImpl { private var members: [PeerInfoMember] = [] private var dataState: PeerInfoMembersDataState = .loading(isInitial: true) private var removingMemberIds: [PeerId: Disposable] = [:] + private var membersHidden: Bool? private let stateValue = Promise() var state: Signal { @@ -148,7 +149,7 @@ private final class PeerInfoMembersContextImpl { self.pushState() if peerId.namespace == Namespaces.Peer.CloudChannel { - let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { [weak self] state in + let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, requestUpdate: true, updated: { [weak self] state in queue.async { guard let strongSelf = self else { return @@ -178,6 +179,7 @@ private final class PeerInfoMembersContextImpl { guard let strongSelf = self else { return } + if let channel = peerViewMainPeer(view) as? TelegramChannel { var canAddMembers = false switch channel.info { @@ -191,6 +193,20 @@ private final class PeerInfoMembersContextImpl { strongSelf.canAddMembers = canAddMembers strongSelf.pushState() } + + var membersHidden: Bool? + if let cachedData = view.cachedData as? CachedChannelData, case let .known(value) = cachedData.membersHidden { + membersHidden = value.value + } + + if strongSelf.membersHidden != membersHidden { + let shouldResetList = strongSelf.membersHidden != nil + strongSelf.membersHidden = membersHidden + + if shouldResetList, let control = strongSelf.channelMembersControl { + context.peerChannelMemberCategoriesContextsManager.reset(peerId: peerId, control: control) + } + } })) } else if peerId.namespace == Namespaces.Peer.CloudGroup { self.disposable.set((context.account.postbox.peerView(id: peerId) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift index ed2435ca9a3..b1d9e550170 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoPaneContainerNode.swift @@ -8,6 +8,7 @@ import Postbox import TelegramCore import AccountContext import ContextUI +import ChatControllerInteraction protocol PeerInfoPaneNode: ASDisplayNode { var isReady: Signal { get } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 440c0359546..32911640f7b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -93,6 +93,16 @@ import ChatTimerScreen import NotificationPeerExceptionController import StickerPackPreviewUI import ChatListHeaderComponent +import ChatControllerInteraction +import StorageUsageScreen + +enum PeerInfoAvatarEditingMode { + case generic + case accept + case suggest + case custom + case fallback +} protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -500,6 +510,9 @@ private final class PeerInfoInteraction { let editingOpenSoundSettings: () -> Void let editingToggleShowMessageText: (Bool) -> Void let requestDeleteContact: () -> Void + let suggestPhoto: () -> Void + let setCustomPhoto: () -> Void + let resetCustomPhoto: () -> Void let openAddContact: () -> Void let updateBlocked: (Bool) -> Void let openReport: (PeerInfoReportType) -> Void @@ -546,6 +559,9 @@ private final class PeerInfoInteraction { editingOpenSoundSettings: @escaping () -> Void, editingToggleShowMessageText: @escaping (Bool) -> Void, requestDeleteContact: @escaping () -> Void, + suggestPhoto: @escaping () -> Void, + setCustomPhoto: @escaping () -> Void, + resetCustomPhoto: @escaping () -> Void, openChat: @escaping () -> Void, openAddContact: @escaping () -> Void, updateBlocked: @escaping (Bool) -> Void, @@ -592,6 +608,9 @@ private final class PeerInfoInteraction { self.editingOpenSoundSettings = editingOpenSoundSettings self.editingToggleShowMessageText = editingToggleShowMessageText self.requestDeleteContact = requestDeleteContact + self.suggestPhoto = suggestPhoto + self.setCustomPhoto = setCustomPhoto + self.resetCustomPhoto = resetCustomPhoto self.openChat = openChat self.openAddContact = openAddContact self.updateBlocked = updateBlocked @@ -1451,7 +1470,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese return result } -private func editingItems(data: PeerInfoScreenData?, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { enum Section: Int, CaseIterable { case notifications case groupLocation @@ -1468,8 +1487,62 @@ private func editingItems(data: PeerInfoScreenData?, chatLocation: ChatLocation, } if let data = data { - if let _ = data.peer as? TelegramUser { - let ItemDelete = 0 + if let user = data.peer as? TelegramUser { + let ItemSuggest = 0 + let ItemCustom = 1 + let ItemReset = 2 + let ItemInfo = 3 + let ItemDelete = 4 + + if !user.flags.contains(.isSupport) { + let compactName = EnginePeer(user).compactDisplayTitle + items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemSuggest, text: presentationData.strings.UserInfo_SuggestPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Peer Info/SuggestAvatar"), action: { + interaction.suggestPhoto() + })) + + let setText: String + if user.photo.first?.isPersonal == true || state.updatingAvatar != nil { + setText = presentationData.strings.UserInfo_ChangeCustomPhoto(compactName).string + } else { + setText = presentationData.strings.UserInfo_SetCustomPhoto(compactName).string + } + + items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemCustom, text: setText, color: .accent, icon: UIImage(bundleImageName: "Settings/SetAvatar"), action: { + interaction.setCustomPhoto() + })) + + if user.photo.first?.isPersonal == true || state.updatingAvatar != nil { + var representation: TelegramMediaImageRepresentation? + var originalIsVideo: Bool? + if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.photo { + representation = photo?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)) + originalIsVideo = !(photo?.videoRepresentations.isEmpty ?? true) + } + + let removeText: String + if let originalIsVideo { + removeText = originalIsVideo ? presentationData.strings.UserInfo_ResetCustomVideo : presentationData.strings.UserInfo_ResetCustomPhoto + } else { + removeText = user.photo.first?.hasVideo == true ? presentationData.strings.UserInfo_RemoveCustomVideo : presentationData.strings.UserInfo_RemoveCustomPhoto + } + + let imageSignal: Signal + if let representation, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(user), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 28.0, height: 28.0)) { + imageSignal = signal + |> map { data -> UIImage? in + return data?.0 + } + } else { + imageSignal = peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)) + } + + items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: imageSignal, action: { + interaction.resetCustomPhoto() + })) + } + items[.peerDataSettings]!.append(PeerInfoScreenCommentItem(id: ItemInfo, text: presentationData.strings.UserInfo_CustomPhotoInfo(compactName).string)) + } + if data.isContact { items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemDelete, text: presentationData.strings.UserInfo_DeleteContact, color: .destructive, action: { interaction.requestDeleteContact() @@ -2110,6 +2183,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate requestDeleteContact: { [weak self] in self?.requestDeleteContact() }, + suggestPhoto: { [weak self] in + self?.suggestPhoto() + }, + setCustomPhoto: { [weak self] in + self?.setCustomPhoto() + }, + resetCustomPhoto: { [weak self] in + self?.resetCustomPhoto() + }, openChat: { [weak self] in self?.openChat() }, @@ -2949,13 +3031,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, sourceCorners: .round, remoteEntries: entriesPromise, skipInitial: true, centralEntryIndex: centralEntry.flatMap { entries.firstIndex(of: $0) }, replaceRootController: { controller, ready in }) galleryController.openAvatarSetup = { [weak self] completion in - self?.openAvatarForEditing(fromGallery: true, completion: completion) + self?.openAvatarForEditing(fromGallery: true, completion: { _ in + completion() + }) } galleryController.avatarPhotoEditCompletion = { [weak self] image in - self?.updateProfilePhoto(image) + self?.updateProfilePhoto(image, mode: .generic) } galleryController.avatarVideoEditCompletion = { [weak self] image, asset, adjustments in - self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) + self?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: .generic) } galleryController.removedEntry = { [weak self] entry in if let item = PeerInfoAvatarListItem(entry: entry) { @@ -3158,7 +3242,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let firstName = strongSelf.headerNode.editingContentNode.editingTextForKey(.firstName) ?? "" let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? "" - if peer.firstName != firstName || peer.lastName != lastName { + if (peer.firstName ?? "") != firstName || (peer.lastName ?? "") != lastName { if firstName.isEmpty && lastName.isEmpty { if strongSelf.hapticFeedback == nil { strongSelf.hapticFeedback = HapticFeedback() @@ -3556,11 +3640,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return } + var isPersonal = false var currentIsVideo = false let item = strongSelf.headerNode.avatarListNode.listContainerNode.currentItemNode?.item - if let item = item, case let .image(_, _, videoRepresentations, _) = item { + if let item = item, case let .image(_, representations, videoRepresentations, _, _) = item { + if representations.first?.representation.isPersonal == true { + isPersonal = true + } currentIsVideo = !videoRepresentations.isEmpty } + guard !isPersonal else { + return + } let items: [ContextMenuItem] = [ .action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in @@ -3596,7 +3687,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let file = files.first?.value { var stickerPackReference: StickerPackReference? for attribute in file.attributes { - if case let .CustomEmoji(_, _, packReference) = attribute { + if case let .CustomEmoji(_, _, _, packReference) = attribute { stickerPackReference = packReference break } @@ -3765,6 +3856,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate var previousAbout: String? var currentAbout: String? + var previousPhotoIsPersonal: Bool? + var currentPhotoIsPersonal: Bool? + if let previousUser = previousData?.peer as? TelegramUser { + previousPhotoIsPersonal = previousUser.profileImageRepresentations.first?.isPersonal == true + } + if let user = data.peer as? TelegramUser { + currentPhotoIsPersonal = user.profileImageRepresentations.first?.isPersonal == true + } + if let previousCachedData = previousData?.cachedData as? CachedChannelData, let cachedData = data.cachedData as? CachedChannelData { previousCall = previousCachedData.activeCall currentCall = cachedData.activeCall @@ -3801,6 +3901,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if (previousAbout?.isEmpty ?? true) != (currentAbout?.isEmpty ?? true) { infoUpdated = true } + if let previousPhotoIsPersonal, let currentPhotoIsPersonal, previousPhotoIsPersonal != currentPhotoIsPersonal { + infoUpdated = true + } self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: self.didSetReady && (membersUpdated || infoUpdated) ? .animated(duration: 0.3, curve: .spring) : .immediate) } } @@ -3940,17 +4043,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: message.associatedThreadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(), snapshots: snapshots, transitionCompletion: { transitionCompletion() - }, presentStickers: { [weak self] completion in - if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.controller?.present(controller, in: .window(.root)) - return controller - } else { - return nil - } }, getCaptionPanelView: { return nil }, sendMessagesWithSignals: { [weak self] signals, _, _ in @@ -4863,9 +4955,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } var canReport = true - if channel.isVerified { - canReport = false - } if channel.adminRights != nil { canReport = false } @@ -6892,11 +6981,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } - private func updateProfilePhoto(_ image: UIImage) { + fileprivate func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) { guard let data = image.jpegData(compressionQuality: 0.6) else { return } - + if self.headerNode.isAvatarExpanded { self.headerNode.ignoreCollapse = true self.headerNode.updateIsAvatarExpanded(false, transition: .immediate) @@ -6906,20 +6995,62 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false) + + if [.suggest, .fallback].contains(mode) { + } else { + self.state = self.state.withUpdatingAvatar(.image(representation)) + } - self.state = self.state.withUpdatingAvatar(.image(representation)) if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false) } self.headerNode.ignoreCollapse = false let postbox = self.context.account.postbox - let signal = self.isSettings ? self.context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) + let signal: Signal + if self.isSettings { + if case .fallback = mode { + signal = self.context.engine.accountData.updateFallbackPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } else { + signal = self.context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } + } else if case .custom = mode { + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } else if case .suggest = mode { + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } else { + signal = self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } + + var dismissStatus: (() -> Void)? + if [.suggest, .fallback, .accept].contains(mode) { + let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in + self?.updateAvatarDisposable.set(nil) + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + statusController?.dismiss() + } + if let topController = self.controller?.navigationController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else if let topController = self.controller?.parentController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else { + self.controller?.presentInGlobalOverlay(statusController) + } + } + self.updateAvatarDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { @@ -6934,10 +7065,42 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) } + + if case .complete = result { + dismissStatus?() + + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer { + switch mode { + case .fallback: + (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + case .custom: + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() + case .suggest: + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in + })) + } + case .accept: + (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if case .info = action { + self?.controller?.parentController?.openSettings() + } + return false + }), in: .current) + default: + break + } + } + }) + } })) } - private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { + fileprivate func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?, mode: PeerInfoAvatarEditingMode) { guard let data = image.jpegData(compressionQuality: 0.6) else { return } @@ -6951,11 +7114,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false) - self.state = self.state.withUpdatingAvatar(.image(representation)) + if [.suggest, .fallback].contains(mode) { + } else { + self.state = self.state.withUpdatingAvatar(.image(representation)) + } + if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: mode == .custom ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, additive: false) } self.headerNode.ignoreCollapse = false @@ -6976,9 +7143,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let uploadInterface = LegacyLiveUploadInterface(context: context) let signal: SSignal - if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else if let url = asset as? URL, let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { + if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { let durationSignal: SSignal = SSignal(generator: { subscriber in let disposable = (entityRenderer.duration()).start(next: { duration in subscriber.putNext(duration) @@ -6997,6 +7162,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } }) + } else if let asset = asset as? AVAsset { + signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, watcher: uploadInterface, entityRenderer: entityRenderer)! } else { signal = SSignal.complete() } @@ -7045,12 +7212,44 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } + var dismissStatus: (() -> Void)? + if [.suggest, .fallback, .accept].contains(mode) { + let statusController = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { [weak self] in + self?.updateAvatarDisposable.set(nil) + dismissStatus?() + })) + dismissStatus = { [weak statusController] in + statusController?.dismiss() + } + if let topController = self.controller?.navigationController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else if let topController = self.controller?.parentController?.topViewController as? ViewController { + topController.presentInGlobalOverlay(statusController) + } else { + self.controller?.presentInGlobalOverlay(statusController) + } + } + let peerId = self.peerId let isSettings = self.isSettings self.updateAvatarDisposable.set((signal |> mapToSignal { videoResource -> Signal in if isSettings { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + if case .fallback = mode { + return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else { + return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } + } else if case .custom = mode { + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + }) + } else if case .suggest = mode { + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) }) } else { @@ -7072,6 +7271,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) } + + if case .complete = result { + dismissStatus?() + + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self, let peer { + switch mode { + case .fallback: + (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + case .custom: + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + + let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).start() + case .suggest: + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in + })) + } + case .accept: + (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undo: false), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + if case .info = action { + self?.controller?.parentController?.openSettings() + } + return false + }), in: .current) + default: + break + } + } + }) + } })) } @@ -7149,14 +7380,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate // } } - private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) { - guard let peer = self.data?.peer, canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else { + fileprivate func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { + guard let peer = self.data?.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else { return } var currentIsVideo = false let item = self.headerNode.avatarListNode.listContainerNode.currentItemNode?.item - if let item = item, case let .image(_, _, videoRepresentations, _) = item { + if let item = item, case let .image(_, _, videoRepresentations, _, _) = item { currentIsVideo = !videoRepresentations.isEmpty } @@ -7183,7 +7414,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate legacyController.bind(controller: navigationController) strongSelf.view.endEditing(true) - strongSelf.controller?.present(legacyController, in: .window(.root)) + (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.present(legacyController, in: .window(.root)) var hasPhotos = false if !peer.profileImageRepresentations.isEmpty { @@ -7191,50 +7422,93 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) - paintStickersContext.presentStickersController = { completion in - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(fileReference.media) - completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) - return true - }) - strongSelf.controller?.present(controller, in: .window(.root)) - return controller - } - + var isForum = false if let peer = strongSelf.data?.peer as? TelegramChannel, peer.flags.contains(.isForum) { isForum = true } - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: strongSelf.isSettings, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum)! + var hasDeleteButton = false + if case .generic = mode { + hasDeleteButton = hasPhotos && !fromGallery + } else if case .custom = mode { + hasDeleteButton = peer.profileImageRepresentations.first?.isPersonal == true + } else if case .fallback = mode { + if let cachedData = strongSelf.data?.cachedData as? CachedUserData, case let .known(photo) = cachedData.fallbackPhoto { + hasDeleteButton = photo != nil + } + } + + let title: String? + let confirmationTextPhoto: String? + let confirmationTextVideo: String? + let confirmationAction: String? + switch mode { + case .suggest: + title = strongSelf.presentationData.strings.UserInfo_SuggestPhotoTitle(peer.compactDisplayTitle).string + confirmationTextPhoto = strongSelf.presentationData.strings.UserInfo_SuggestPhoto_AlertPhotoText(peer.compactDisplayTitle).string + confirmationTextVideo = strongSelf.presentationData.strings.UserInfo_SuggestPhoto_AlertVideoText(peer.compactDisplayTitle).string + confirmationAction = strongSelf.presentationData.strings.UserInfo_SuggestPhoto_AlertSuggest + case .custom: + title = strongSelf.presentationData.strings.UserInfo_SetCustomPhotoTitle(peer.compactDisplayTitle).string + confirmationTextPhoto = strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_AlertPhotoText(peer.compactDisplayTitle, peer.compactDisplayTitle).string + confirmationTextVideo = strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_AlertVideoText(peer.compactDisplayTitle, peer.compactDisplayTitle).string + confirmationAction = strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_AlertSet + default: + title = nil + confirmationTextPhoto = nil + confirmationTextVideo = nil + confirmationAction = nil + } + + let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))! mixin.stickersContext = paintStickersContext let _ = strongSelf.currentAvatarMixin.swap(mixin) mixin.requestSearchController = { [weak self] assetsController in guard let strongSelf = self else { return } - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: strongSelf.isSettings ? nil : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: strongSelf.isSettings ? nil : peer.compactDisplayTitle, completion: { [weak self] result in assetsController?.dismiss() - self?.updateProfilePhoto(result) + self?.updateProfilePhoto(result, mode: mode) })) controller.navigationPresentation = .modal - strongSelf.controller?.push(controller) + (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) if fromGallery { - completion() + completion(nil) + } + } + if let confirmationTextPhoto, let confirmationAction { + mixin.willFinishWithImage = { [weak self] image, commit in + if let strongSelf = self, let image { + let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextPhoto, doneTitle: confirmationAction, commit: { + commit?() + }) + (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller) + } + } + } + if let confirmationTextVideo, let confirmationAction { + mixin.willFinishWithVideo = { [weak self] image, commit in + if let strongSelf = self, let image { + let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextVideo, doneTitle: confirmationAction, commit: { + commit?() + }) + (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller) + } } } mixin.didFinishWithImage = { [weak self] image in if let image = image { - completion() - self?.updateProfilePhoto(image) + completion(image) + self?.updateProfilePhoto(image, mode: mode) } } mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in if let image = image, let asset = asset { - completion() - self?.updateProfileVideo(image, asset: asset, adjustments: adjustments) + completion(image) + self?.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) } } mixin.didFinishWithDelete = { @@ -7242,55 +7516,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return } - let proceed = { - if let item = item { - strongSelf.deleteProfilePhoto(item) - } - - let _ = strongSelf.currentAvatarMixin.swap(nil) - if let _ = peer.smallProfileImage { - strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } - } - let postbox = strongSelf.context.account.postbox - strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.peers.updatePeerPhoto(peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) - |> deliverOnMainQueue).start(next: { result in - guard let strongSelf = self else { - return - } - switch result { - case .complete: - strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } - case .progress: - break - } - })) - } - - let actionSheet = ActionSheetController(presentationData: presentationData) - let items: [ActionSheetItem] = [ - ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - proceed() - }) - ] - - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + strongSelf.openAvatarRemoval(mode: mode, peer: peer, item: item) } mixin.didDismiss = { [weak legacyController] in guard let strongSelf = self else { @@ -7308,6 +7534,81 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }) } + fileprivate func openAvatarRemoval(mode: PeerInfoAvatarEditingMode, peer: EnginePeer? = nil, item: PeerInfoAvatarListItem? = nil, completion: @escaping () -> Void = {}) { + let proceed = { [weak self] in + guard let strongSelf = self else { + return + } + + completion() + + if let item = item { + strongSelf.deleteProfilePhoto(item) + } + + let _ = strongSelf.currentAvatarMixin.swap(nil) + if mode != .fallback { + if let peer = peer, let _ = peer.smallProfileImage { + strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + } + let postbox = strongSelf.context.account.postbox + let signal: Signal + if case .custom = mode { + signal = strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } else if case .fallback = mode { + signal = strongSelf.context.engine.accountData.removeFallbackPhoto(reference: nil) + |> castError(UploadPeerPhotoError.self) + |> map { _ in + return .complete([]) + } + } else { + signal = strongSelf.context.engine.peers.updatePeerPhoto(peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + }) + } + strongSelf.updateAvatarDisposable.set((signal + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + switch result { + case .complete: + strongSelf.state = strongSelf.state.withUpdatingAvatar(nil) + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + case .progress: + break + } + })) + } + + let presentationData = self.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + let items: [ActionSheetItem] = [ + ActionSheetButtonItem(title: presentationData.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + proceed() + }) + ] + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + (self.controller?.navigationController?.topViewController as? ViewController)?.present(actionSheet, in: .window(.root)) + } + private func openAddMember() { guard let data = self.data, let groupPeer = data.peer, let controller = self.controller else { return @@ -7413,6 +7714,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } ) } + }, requestPublicPhotoSetup: { [weak self] completion in + if let strongSelf = self { + strongSelf.openAvatarForEditing(mode: .fallback, completion: completion) + } + }, requestPublicPhotoRemove: { [weak self] completion in + if let strongSelf = self { + strongSelf.openAvatarRemoval(mode: .fallback, completion: completion) + } })) } }) @@ -7479,8 +7788,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let strongSelf = self else { return } - // MARK: Nicegram max accounts - let maximumAvailableAccounts: Int = 100 + // MARK: Nicegram MaxAccounts + let maximumAvailableAccounts: Int = nicegramMaximumNumberOfAccounts var count: Int = 1 for (accountContext, _, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { @@ -7747,7 +8056,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate func forwardMessages(messageIds: Set?) { if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { - let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true)) + let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: true, selectForumThreads: true)) peerSelectionController.multiplePeersSelected = { [weak self, weak peerSelectionController] peers, peerMap, messageText, mode, forwardOptions in guard let strongSelf = self, let strongController = peerSelectionController else { return @@ -8325,6 +8634,44 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate peerController.present(controller, in: .window(.root)) } + private func suggestPhoto() { + self.openAvatarForEditing(mode: .suggest) + } + + private func setCustomPhoto() { + self.openAvatarForEditing(mode: .custom) + } + + private func resetCustomPhoto() { + guard let peer = self.data?.peer else { + return + } + let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.UserInfo_ResetToOriginalAlertText(EnginePeer(peer).compactDisplayTitle).string, actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + + }), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.UserInfo_ResetToOriginalAlertReset, action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + mapResourceToAvatarSizes(postbox: strongSelf.context.account.postbox, resource: resource, representations: representations) + }) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.state = strongSelf.state.withUpdatingAvatar(nil).withAvatarUploadProgress(nil) + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), additive: false) + } + })) + }) + ]) + self.controller?.present(alertController, in: .window(.root)) + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData @@ -8455,7 +8802,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } var validEditingSections: [AnyHashable] = [] - let editItems = self.isSettings ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction) : editingItems(data: self.data, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction) + let editItems = self.isSettings ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction) : editingItems(data: self.data, state: self.state, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction) for (sectionId, sectionItems) in editItems { var insets = UIEdgeInsets() @@ -9012,6 +9359,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private let chatLocation: ChatLocation private let chatLocationContextHolder = Atomic(value: nil) + weak var parentController: TelegramRootController? + fileprivate var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let cachedDataPromise = Promise() @@ -9424,6 +9773,20 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } + func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) { + if !self.isNodeLoaded { + self.loadDisplayNode() + } + self.controllerNode.updateProfilePhoto(image, mode: mode) + } + + func updateProfileVideo(_ image: UIImage, mode: PeerInfoAvatarEditingMode, asset: Any?, adjustments: TGVideoEditAdjustments?, fallback: Bool = false) { + if !self.isNodeLoaded { + self.loadDisplayNode() + } + self.controllerNode.updateProfileVideo(image, asset: asset, adjustments: adjustments, mode: mode) + } + static func displayChatNavigationMenu(context: AccountContext, chatNavigationStack: [ChatNavigationStackItem], nextFolderId: Int32?, parentController: ViewController, backButtonView: UIView, navigationController: NavigationController, gesture: ContextGesture) { let peerMap = EngineDataMap( Set(chatNavigationStack.map(\.peerId)).map(TelegramEngine.EngineData.Item.Peer.Peer.init) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PhotoUpdateConfirmationController.swift b/submodules/TelegramUI/Sources/PeerInfo/PhotoUpdateConfirmationController.swift new file mode 100644 index 00000000000..61046ab4e8e --- /dev/null +++ b/submodules/TelegramUI/Sources/PeerInfo/PhotoUpdateConfirmationController.swift @@ -0,0 +1,244 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode + +private final class PhotoUpdateConfirmationAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let text: String + + private let textNode: ASTextNode + private let avatarNode: AvatarNode + private let arrowNode: ASImageNode + private let iconNode: ASImageNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, image: UIImage, text: String, actions: [TextAlertAction]) { + self.strings = strings + self.text = text + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.iconNode = ASImageNode() + self.iconNode.clipsToBounds = true + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.image = image + self.iconNode.cornerRadius = 30.0 + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.iconNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + if let _ = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + origin.y += avatarSize.height + 10.0 + } + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func photoUpdateConfirmationController(context: AccountContext, peer: EnginePeer, image: UIImage, text: String, doneTitle: String, commit: @escaping () -> Void) -> AlertController { + let theme = defaultDarkColorPresentationTheme + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: theme) + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + var contentNode: PhotoUpdateConfirmationAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: doneTitle, action: { + dismissImpl?(true) + commit() + })] + + contentNode = PhotoUpdateConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, image: image, text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 098d790c025..a24126c45b5 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -14,6 +14,7 @@ import UniversalMediaPlayer import ListMessageItem import ChatMessageInteractiveMediaBadge import SoftwareVideo +import ChatControllerInteraction private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext @@ -34,6 +35,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { init( context: AccountContext, + userLocation: MediaResourceUserLocation, file: FileMediaReference ) { self.context = context @@ -62,6 +64,8 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, + userLocation: userLocation, + userContentType: .other, fileReference: self.file, automaticallyFetchHeader: true ) @@ -297,7 +301,7 @@ private final class VisualMediaItemNode: ASDisplayNode { self.imageNode.layer.addSublayer(sampleBufferLayer.layer) } - self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer) + self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .other, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer) self.videoLayerFrameManager?.start() } } else { @@ -313,7 +317,7 @@ private final class VisualMediaItemNode: ASDisplayNode { if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize.cgSize - self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) + self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) self.fetchStatusDisposable.set(nil) self.statusNode.transitionToState(.none, completion: { [weak self] in @@ -323,7 +327,7 @@ private final class VisualMediaItemNode: ASDisplayNode { self.resourceStatus = nil } else if let file = media as? TelegramMediaFile, file.isVideo { mediaDimensions = file.dimensions?.cgSize - self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) + self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) self.mediaBadgeNode.isHidden = file.isAnimated diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index 9a2f0c710e0..e2bdace9384 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -22,6 +22,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon public var multiplePeersSelected: (([Peer], [PeerId: Peer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? private let filter: ChatListNodePeersFilter private let forumPeerId: EnginePeer.Id? + private let selectForumThreads: Bool private let attemptSelection: ((Peer, Int64?) -> Void)? private let createNewGroup: (() -> Void)? @@ -91,6 +92,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.pretendPresentedInModal = params.pretendPresentedInModal self.forwardedMessageIds = params.forwardedMessageIds self.hasTypeHeaders = params.hasTypeHeaders + self.selectForumThreads = params.selectForumThreads super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -181,7 +183,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.peerSelectionNode.requestOpenPeer = { [weak self] peer, threadId in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { - if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum), threadId == nil { + if let peer = peer as? TelegramChannel, peer.flags.contains(.isForum), threadId == nil, strongSelf.selectForumThreads { let controller = PeerSelectionControllerImpl( PeerSelectionControllerParams( context: strongSelf.context, @@ -197,7 +199,9 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon pretendPresentedInModal: false, multipleSelection: false, forwardedMessageIds: [], - hasTypeHeaders: false) + hasTypeHeaders: false, + selectForumThreads: false + ) ) controller.peerSelected = strongSelf.peerSelected strongSelf.push(controller) diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 774912c9a47..676b2ff2871 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -183,7 +183,7 @@ private final class PrefetchManagerInnerImpl { } } else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let _ = file.size { - context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start()) + context.fetchDisposable.set(preloadVideoResource(postbox: self.account.postbox, userLocation: .peer(mediaItem.media.index.id.peerId), userContentType: MediaResourceUserContentType(file: file), resourceReference: FileMediaReference.message(message: MessageReference(peer: mediaItem.media.peer, author: nil, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), media: file).resourceReference(file.resource), duration: 4.0).start()) } } } @@ -251,7 +251,7 @@ private final class PrefetchManagerInnerImpl { self.preloadGreetingStickerDisposable.set((self.preloadedGreetingStickerPromise.get() |> mapToSignal { sticker -> Signal in if let sticker = sticker { - let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: sticker)).start() + let _ = freeMediaFileInteractiveFetched(account: account, userLocation: .other, fileReference: .standalone(media: sticker)).start() return chatMessageAnimationData(mediaBox: account.postbox.mediaBox, resource: sticker.resource, fitzModifier: nil, isVideo: sticker.isVideoSticker, width: 384, height: 384, synchronousLoad: false) |> mapToSignal { _ -> Signal in return .complete() diff --git a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift index 7e6659bacee..d516efebf7b 100644 --- a/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift +++ b/submodules/TelegramUI/Sources/PreparedChatHistoryViewTransition.swift @@ -5,6 +5,7 @@ import TelegramCore import Display import MergeLists import AccountContext +import ChatControllerInteraction func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, reverse: Bool, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, scrollAnimationCurve: ListViewAnimationCurve?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?, cachedDataMessages: [MessageId: Message]?, readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, flashIndicators: Bool, updatedMessageSelection: Bool, messageTransitionNode: ChatMessageTransitionNode?, allUpdated: Bool) -> ChatHistoryViewTransition { var mergeResult: (deleteIndices: [Int], indicesAndItems: [(Int, ChatHistoryEntry, Int?)], updateIndices: [(Int, ChatHistoryEntry, Int)]) diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index ee26fb232cf..836338c3eed 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -205,16 +205,18 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } strongSelf.previousMediaReference = updatedMediaReference + let hasSpoiler = message?.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) ?? false + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - updateImageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference) + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference, blurred: hasSpoiler) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { - updateImageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference) + updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - updateImageSignal = chatWebpageSnippetFile(account: context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: (message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } else { diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index 16d76f96f96..ce6fe27f3a0 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -675,7 +675,7 @@ public class ShareRootControllerImpl { attemptSelectionImpl?(peer) }, createNewGroup: { createNewGroupImpl?() - }, pretendPresentedInModal: true)) + }, pretendPresentedInModal: true, selectForumThreads: true)) controller.customDismiss = { self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) @@ -849,7 +849,7 @@ public class ShareRootControllerImpl { var attemptSelectionImpl: ((Peer) -> Void)? let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeDisabled, .doNotSearchMessages, .excludeSecretChats], hasChatListSelector: false, hasContactSelector: true, hasGlobalSearch: false, title: presentationData.strings.ChatImport_Title, attemptSelection: { peer, _ in attemptSelectionImpl?(peer) - }, pretendPresentedInModal: true)) + }, pretendPresentedInModal: true, selectForumThreads: true)) controller.customDismiss = { self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) @@ -924,7 +924,7 @@ public class ShareRootControllerImpl { attemptSelectionImpl?(peer) }, createNewGroup: { createNewGroupImpl?() - }, pretendPresentedInModal: true)) + }, pretendPresentedInModal: true, selectForumThreads: true)) controller.customDismiss = { self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index c24ed95a9ab..0f34c1a1a03 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -28,6 +28,8 @@ import WallpaperBackgroundNode import InAppPurchaseManager import PremiumUI import StickerPackPreviewUI +import ChatControllerInteraction +import ChatPresentationInterfaceState import NGData @@ -1620,10 +1622,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } - + public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { return proxySettingsController(accountManager: sharedContext.accountManager, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) } + + public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode) -> ViewController { + return installedStickerPacksController(context: context, mode: mode) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 12038021d6e..d9ccccc3368 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -228,13 +228,13 @@ final class SharedMediaPlayer { case .voice, .music: switch playbackData.source { case let .telegramFile(fileReference, _): - strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity)) + strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity)) } case .instantVideo: if let mediaManager = strongSelf.mediaManager, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { case let .telegramFile(fileReference, _): - let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), fileReference: fileReference, enableSound: false, baseRate: rateValue, captureProtected: item.message.isCopyProtected()), close: { [weak mediaManager] in + let videoNode = OverlayInstantVideoNode(postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, captureProtected: item.message.isCopyProtected()), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) strongSelf.playbackItem = .instantVideo(videoNode) @@ -497,7 +497,7 @@ final class SharedMediaPlayer { } switch next { case let .telegramFile(file, _): - fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, reference: file.resourceReference(file.media.resource)) + fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, userLocation: .other, userContentType: .audio, reference: file.resourceReference(file.media.resource)) |> ignoreValues |> `catch` { _ -> Signal in return .complete() diff --git a/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift index d7e50225e77..243473a3bec 100644 --- a/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift +++ b/submodules/TelegramUI/Sources/StickerPaneTrendingListGridItem.swift @@ -12,6 +12,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import MergeLists +import ChatPresentationInterfaceState private let boundingSize = CGSize(width: 41.0, height: 41.0) private let boundingImageSize = CGSize(width: 28.0, height: 28.0) @@ -266,7 +267,7 @@ private final class FeaturedPackItemNode: ListViewItemNode { thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } @@ -310,7 +311,7 @@ private final class FeaturedPackItemNode: ListViewItemNode { animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers } if let resourceReference = resourceReference { - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: resourceReference).start()) + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference).start()) } } } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift index 2ee6fa30c7c..9756e35f081 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift @@ -219,8 +219,8 @@ final class StickersChatInputContextPanelItemNode: ListViewItemNode { strongSelf.addSubnode(imageNode) } - imageNode.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) - strongSelf.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start()) + imageNode.setSignal(chatMessageSticker(account: item.account, userLocation: .other, file: file, small: true)) + strongSelf.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start()) var imageSize = itemSize if let dimensions = file.dimensions { diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 116aaec010d..6c2ea057b05 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -15,6 +15,7 @@ import ContextUI import ChatPresentationInterfaceState import PremiumUI import UndoUI +import ChatControllerInteraction private struct StickersChatInputContextPanelEntryStableId: Hashable { let ids: [MediaId] diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index c56fee73ffb..18b0f0d329c 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -18,7 +18,7 @@ import AppBundle import DatePickerNode import DebugSettingsUI import TabBarUI -import PremiumUI +import DrawingUI public final class TelegramRootController: NavigationController { private let context: AccountContext @@ -139,8 +139,9 @@ public final class TelegramRootController: NavigationController { } strongSelf.pushViewController(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context)) } + accountSettingsController.parentController = self controllers.append(accountSettingsController) - + tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : (controllers.count - 2)) self.contactsController = contactsController @@ -199,4 +200,16 @@ public final class TelegramRootController: NavigationController { controller.view.endEditing(true) presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller) } + + public func openSettings() { + guard let rootTabController = self.rootTabController else { + return + } + + self.popToRoot(animated: false) + + if let index = rootTabController.controllers.firstIndex(where: { $0 is PeerInfoScreenImpl }) { + rootTabController.selectedIndex = index + } + } } diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index ef182826550..63c96cff23d 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -80,7 +80,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate let packReference: StickerPackReference = .name(name) controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) case let .instantView(webpage, anchor): - (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .group, anchor: anchor)) + (controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group), anchor: anchor)) case let .join(link): controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) diff --git a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift index 674a0a82b4b..f61684fdec3 100644 --- a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift @@ -103,7 +103,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { |> mapToSignal { wallpaper -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in if let wallpaper = wallpaper, case let .file(file) = wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationThemeReference, PresentationTheme?), NoError> in guard complete, let fullSizeData = fullSizeData else { diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index 8e51acf4b82..2b20f1e5252 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -11,7 +11,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me switch media.media { case let file as TelegramMediaFile: let signal = Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: media.resourceReference(file.resource)).start() + let fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: media.resourceReference(file.resource)).start() let data = postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) if next.complete { @@ -74,7 +74,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } } attributes.append(.ImageSize(size: PixelDimensions(imageDimensions))) - let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)]).withUpdatedAttributes(attributes) + let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)]).withUpdatedAttributes(attributes) subscriber.putNext(.standalone(media: updatedFile)) subscriber.putCompletion() } else { @@ -103,7 +103,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) - let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)]) + let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)]) subscriber.putNext(.standalone(media: updatedFile)) subscriber.putCompletion() } else { @@ -127,7 +127,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me case let image as TelegramMediaImage: if let representation = largestImageRepresentation(image.representations) { let signal = Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: media.resourceReference(representation.resource)).start() + let fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .image, reference: media.resourceReference(representation.resource)).start() let data = postbox.mediaBox.resourceData(representation.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in subscriber.putNext(next) if next.complete { @@ -159,7 +159,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData) - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) return .single(.standalone(media: updatedImage)) } diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift index 4574657846e..b6ad34f95a2 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift @@ -10,6 +10,7 @@ import MergeLists import AccountContext import SwiftSignalKit import ChatPresentationInterfaceState +import ChatControllerInteraction private enum VerticalChatContextResultsEntryStableId: Hashable { case action diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift index 5550fd87219..5b21e492735 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift @@ -248,11 +248,11 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if updatedIconImageResource { if let imageResource = imageResource { if let stickerFile = stickerFile { - updateIconImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) + updateIconImageSignal = chatMessageSticker(account: item.account, userLocation: .other, file: stickerFile, small: false, fetched: true) } else { - let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false) + let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photoReference: .standalone(media: tmpImage)) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage)) } } else { updateIconImageSignal = .complete() diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 07cfd6ea49b..927e32564b1 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -48,6 +48,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var inlineForums: Bool public var accountReactionEffectOverrides: [AccountReactionOverrides] public var accountStickerEffectOverrides: [AccountReactionOverrides] + public var disableQuickReaction: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -73,7 +74,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableReactionOverrides: false, inlineForums: false, accountReactionEffectOverrides: [], - accountStickerEffectOverrides: [] + accountStickerEffectOverrides: [], + disableQuickReaction: false ) } @@ -99,7 +101,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableReactionOverrides: Bool, inlineForums: Bool, accountReactionEffectOverrides: [AccountReactionOverrides], - accountStickerEffectOverrides: [AccountReactionOverrides] + accountStickerEffectOverrides: [AccountReactionOverrides], + disableQuickReaction: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -123,6 +126,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.inlineForums = inlineForums self.accountReactionEffectOverrides = accountReactionEffectOverrides self.accountStickerEffectOverrides = accountStickerEffectOverrides + self.disableQuickReaction = disableQuickReaction } public init(from decoder: Decoder) throws { @@ -150,6 +154,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.inlineForums = try container.decodeIfPresent(Bool.self, forKey: "inlineForums") ?? false self.accountReactionEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountReactionEffectOverrides")) ?? [] self.accountStickerEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountStickerEffectOverrides")) ?? [] + self.disableQuickReaction = try container.decodeIfPresent(Bool.self, forKey: "disableQuickReaction") ?? false } public func encode(to encoder: Encoder) throws { @@ -177,6 +182,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.inlineForums, forKey: "inlineForums") try container.encode(self.accountReactionEffectOverrides, forKey: "accountReactionEffectOverrides") try container.encode(self.accountStickerEffectOverrides, forKey: "accountStickerEffectOverrides") + try container.encode(self.disableQuickReaction, forKey: "disableQuickReaction") } } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index da4427fe3fb..db42fb3204b 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -36,6 +36,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case webBrowserSettings = 16 case intentsSettings = 17 case translationSettings = 18 + case drawingSettings = 19 } public struct ApplicationSpecificSharedDataKeys { @@ -58,6 +59,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue) public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue) public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue) + public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 21d13bf3cbf..9df648a465f 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -28,6 +28,7 @@ public enum NativeVideoContentId: Hashable { public final class NativeVideoContent: UniversalVideoContent { public let id: AnyHashable public let nativeId: NativeVideoContentId + public let userLocation: MediaResourceUserLocation public let fileReference: FileMediaReference let imageReference: ImageMediaReference? public let dimensions: CGSize @@ -48,9 +49,10 @@ public final class NativeVideoContent: UniversalVideoContent { let captureProtected: Bool let hintDimensions: CGSize? - public init(id: NativeVideoContentId, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false, hintDimensions: CGSize? = nil) { + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false, hintDimensions: CGSize? = nil) { self.id = id self.nativeId = id + self.userLocation = userLocation self.fileReference = fileReference self.imageReference = imageReference if var dimensions = fileReference.media.dimensions { @@ -85,7 +87,7 @@ public final class NativeVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -104,6 +106,7 @@ public final class NativeVideoContent: UniversalVideoContent { private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let postbox: Postbox + private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference private let enableSound: Bool private let loopVideo: Bool @@ -159,8 +162,9 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private var shouldPlay: Bool = false - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, captureProtected: Bool, hintDimensions: CGSize?) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, captureProtected: Bool, hintDimensions: CGSize?) { self.postbox = postbox + self.userLocation = userLocation self.fileReference = fileReference self.placeholderColor = placeholderColor self.enableSound = enableSound @@ -171,7 +175,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.imageNode = TransformImageNode() - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -202,7 +206,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self?.performActionAtEnd() } - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { @@ -268,7 +272,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return } - let thumbnailPlayer = MediaPlayer(audioSessionManager: self.audioSessionManager, postbox: postbox, resourceReference: fileReference.resourceReference(videoThumbnail.resource), tempFilePath: nil, streamable: .none, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: false, baseRate: self.baseRate, fetchAutomatically: false, continuePlayingWithoutSoundOnLostAudioSession: false) + let thumbnailPlayer = MediaPlayer(audioSessionManager: self.audioSessionManager, postbox: postbox, userLocation: self.userLocation, userContentType: MediaResourceUserContentType(file: self.fileReference.media), resourceReference: self.fileReference.resourceReference(videoThumbnail.resource), tempFilePath: nil, streamable: .none, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: false, baseRate: self.baseRate, fetchAutomatically: false, continuePlayingWithoutSoundOnLostAudioSession: false) self.thumbnailPlayer = thumbnailPlayer var actionAtEndImpl: (() -> Void)? @@ -466,7 +470,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func fetchControl(_ control: UniversalVideoNodeFetchControl) { switch control { case .fetch: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: self.fileReference.resourceReference(self.fileReference.media.resource), statsCategory: statsCategoryForFileWithAttributes(self.fileReference.media.attributes)).start()) + self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.postbox.mediaBox, userLocation: self.userLocation, userContentType: .video, reference: self.fileReference.resourceReference(self.fileReference.media.resource), statsCategory: statsCategoryForFileWithAttributes(self.fileReference.media.attributes)).start()) case .cancel: self.postbox.mediaBox.cancelInteractiveResourceFetch(self.fileReference.media.resource) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index c29914a891e..4caf93fbb2d 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -71,6 +71,7 @@ public final class PlatformVideoContent: UniversalVideoContent { public let id: AnyHashable let nativeId: PlatformVideoContentId + let userLocation: MediaResourceUserLocation let content: Content public let dimensions: CGSize public let duration: Int32 @@ -80,8 +81,9 @@ public final class PlatformVideoContent: UniversalVideoContent { let baseRate: Double let fetchAutomatically: Bool - public init(id: PlatformVideoContentId, content: Content, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { + public init(id: PlatformVideoContentId, userLocation: MediaResourceUserLocation, content: Content, streamVideo: Bool = false, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true) { self.id = id + self.userLocation = userLocation self.nativeId = id self.content = content self.dimensions = self.content.dimensions?.cgSize ?? CGSize(width: 480, height: 320) @@ -94,7 +96,7 @@ public final class PlatformVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) + return PlatformVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, content: self.content, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -115,6 +117,7 @@ public final class PlatformVideoContent: UniversalVideoContent { private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private let postbox: Postbox + private let userLocation: MediaResourceUserLocation private let content: PlatformVideoContent.Content private let approximateDuration: Double private let intrinsicDimensions: CGSize @@ -169,11 +172,12 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte private var validLayout: CGSize? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { self.postbox = postbox self.content = content self.approximateDuration = Double(content.duration ?? 1) self.audioSessionManager = audioSessionManager + self.userLocation = userLocation self.imageNode = TransformImageNode() @@ -193,7 +197,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte switch content { case let .file(file): - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, videoReference: file) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: file) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 643a93c7fdd..4e7e45cd939 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -14,21 +14,23 @@ import RangeSet public final class SystemVideoContent: UniversalVideoContent { public let id: AnyHashable + let userLocation: MediaResourceUserLocation let url: String let imageReference: ImageMediaReference public let dimensions: CGSize public let duration: Int32 - public init(url: String, imageReference: ImageMediaReference, dimensions: CGSize, duration: Int32) { + public init(userLocation: MediaResourceUserLocation, url: String, imageReference: ImageMediaReference, dimensions: CGSize, duration: Int32) { self.id = AnyHashable(url) self.url = url + self.userLocation = userLocation self.imageReference = imageReference self.dimensions = dimensions self.duration = duration } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) + return SystemVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, url: self.url, imageReference: self.imageReference, intrinsicDimensions: self.dimensions, approximateDuration: self.duration) } } @@ -81,7 +83,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent private var seekId: Int = 0 - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, url: String, imageReference: ImageMediaReference, intrinsicDimensions: CGSize, approximateDuration: Int32) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, url: String, imageReference: ImageMediaReference, intrinsicDimensions: CGSize, approximateDuration: Int32) { self.audioSessionManager = audioSessionManager self.url = url @@ -104,7 +106,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent super.init() - self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, photoReference: imageReference)) + self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, userLocation: userLocation, photoReference: imageReference)) self.addSubnode(self.imageNode) self.addSubnode(self.playerNode) diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 906645d531b..f95d913ed13 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -14,6 +14,7 @@ import RangeSet public final class WebEmbedVideoContent: UniversalVideoContent { public let id: AnyHashable + let userLocation: MediaResourceUserLocation let webPage: TelegramMediaWebpage public let webpageContent: TelegramMediaWebpageLoadedContent public let dimensions: CGSize @@ -21,11 +22,12 @@ public final class WebEmbedVideoContent: UniversalVideoContent { let forcedTimestamp: Int? let openUrl: (URL) -> Void - public init?(webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) { + public init?(userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) { guard let embedUrl = webpageContent.embedUrl else { return nil } self.id = AnyHashable(embedUrl) + self.userLocation = userLocation self.webPage = webPage self.webpageContent = webpageContent self.dimensions = webpageContent.embedSize?.cgSize ?? CGSize(width: 128.0, height: 128.0) @@ -35,7 +37,7 @@ public final class WebEmbedVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl) + return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl) } } @@ -72,7 +74,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { private var readyDisposable = MetaDisposable() - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) { self.webpageContent = webpageContent if let embedSize = webpageContent.embedSize { @@ -93,7 +95,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { self.addSubnode(self.imageNode) if let image = webpageContent.image { - self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, photoReference: .webPage(webPage: WebpageReference(webPage), media: image))) + self.imageNode.setSignal(chatMessagePhoto(postbox: postbox, userLocation: userLocation, photoReference: .webPage(webPage: WebpageReference(webPage), media: image))) self.imageNode.imageUpdated = { [weak self] _ in self?._ready.set(.single(Void())) } diff --git a/submodules/TelegramVoip/Sources/GroupCallContext.swift b/submodules/TelegramVoip/Sources/GroupCallContext.swift index 4d48062f3f2..6b39e18cae3 100644 --- a/submodules/TelegramVoip/Sources/GroupCallContext.swift +++ b/submodules/TelegramVoip/Sources/GroupCallContext.swift @@ -415,8 +415,9 @@ public final class OngoingGroupCallContext { private final class Impl { let queue: Queue let context: GroupCallThreadLocalContext +#if os(iOS) let audioDevice: SharedCallAudioDevice? - +#endif let sessionId = UInt32.random(in: 0 ..< UInt32(Int32.max)) let joinPayload = Promise<(String, UInt32)>() @@ -434,13 +435,10 @@ public final class OngoingGroupCallContext { init(queue: Queue, inputDeviceId: String, outputDeviceId: String, audioSessionActive: Signal, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, preferX264: Bool, logPath: String) { self.queue = queue +#if os(iOS) self.audioDevice = nil - /*#if DEBUG - self.audioDevice = SharedCallAudioDevice(disableRecording: disableAudioInput) - #else - self.audioDevice = nil - #endif*/ - + let audioDevice = self.audioDevice +#endif var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)? var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)? @@ -455,7 +453,7 @@ public final class OngoingGroupCallContext { } var getBroadcastPartsSource: (() -> BroadcastPartSource?)? - +#if os(iOS) self.context = GroupCallThreadLocalContext( queue: ContextQueueImpl(queue: queue), networkStateUpdated: { state in @@ -547,8 +545,103 @@ public final class OngoingGroupCallContext { disableAudioInput: disableAudioInput, preferX264: preferX264, logPath: logPath, - audioDevice: self.audioDevice + audioDevice: audioDevice ) +#else + self.context = GroupCallThreadLocalContext( + queue: ContextQueueImpl(queue: queue), + networkStateUpdated: { state in + networkStateUpdatedImpl?(state) + }, + audioLevelsUpdated: { levels in + audioLevelsUpdatedImpl?(levels) + }, + inputDeviceId: inputDeviceId, + outputDeviceId: outputDeviceId, + videoCapturer: video?.impl, + requestMediaChannelDescriptions: { ssrcs, completion in + final class OngoingGroupCallMediaChannelDescriptionTaskImpl : NSObject, OngoingGroupCallMediaChannelDescriptionTask { + private let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + func cancel() { + self.disposable.dispose() + } + } + + let disposable = requestMediaChannelDescriptions(Set(ssrcs.map { $0.uint32Value }), { channels in + completion(channels.map { channel -> OngoingGroupCallMediaChannelDescription in + let mappedType: OngoingGroupCallMediaChannelType + switch channel.kind { + case .audio: + mappedType = .audio + case .video: + mappedType = .video + } + return OngoingGroupCallMediaChannelDescription( + type: mappedType, + audioSsrc: channel.audioSsrc, + videoDescription: channel.videoDescription + ) + }) + }) + + return OngoingGroupCallMediaChannelDescriptionTaskImpl(disposable: disposable) + }, + requestCurrentTime: { completion in + let disposable = MetaDisposable() + + queue.async { + disposable.set(getBroadcastPartsSource?()?.requestTime(completion: completion)) + } + + return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) + }, + requestAudioBroadcastPart: { timestampMilliseconds, durationMilliseconds, completion in + let disposable = MetaDisposable() + + queue.async { + disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: { + rejoinNeeded() + })) + } + + return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) + }, + requestVideoBroadcastPart: { timestampMilliseconds, durationMilliseconds, channelId, quality, completion in + let disposable = MetaDisposable() + + queue.async { + let mappedQuality: OngoingGroupCallContext.VideoChannel.Quality + switch quality { + case .thumbnail: + mappedQuality = .thumbnail + case .medium: + mappedQuality = .medium + case .full: + mappedQuality = .full + @unknown default: + mappedQuality = .thumbnail + } + disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: { + rejoinNeeded() + })) + } + + return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable) + }, + outgoingAudioBitrateKbit: outgoingAudioBitrateKbit ?? 32, + videoContentType: _videoContentType, + enableNoiseSuppression: enableNoiseSuppression, + disableAudioInput: disableAudioInput, + preferX264: preferX264, + logPath: logPath + ) +#endif + let queue = self.queue @@ -600,8 +693,8 @@ public final class OngoingGroupCallContext { guard let `self` = self else { return } +// self.audioDevice?.setManualAudioSessionIsActive(isActive) #if os(iOS) - self.audioDevice?.setManualAudioSessionIsActive(isActive) self.context.setManualAudioSessionIsActive(isActive) #endif })) @@ -908,14 +1001,17 @@ public final class OngoingGroupCallContext { } func setTone(tone: Tone?) { + #if os(iOS) let mappedTone = tone.flatMap { tone in CallAudioTone(samples: tone.samples, sampleRate: tone.sampleRate, loopCount: tone.loopCount) } - if let audioDevice = self.audioDevice { - audioDevice.setTone(mappedTone) - } else { +// if let audioDevice = self.audioDevice { +// audioDevice.setTone(mappedTone) +// } else { self.context.setTone(mappedTone) - } +// } + #endif + } } diff --git a/submodules/TelegramVoip/Sources/OngoingCallContext.swift b/submodules/TelegramVoip/Sources/OngoingCallContext.swift index 923e197044f..15ff1892e08 100644 --- a/submodules/TelegramVoip/Sources/OngoingCallContext.swift +++ b/submodules/TelegramVoip/Sources/OngoingCallContext.swift @@ -79,6 +79,11 @@ private func callConnectionDescriptionsWebrtc(_ connection: CallSessionConnectio guard let id = idMapping[reflector.id] else { return [] } + #if DEBUG + if id != 1 { + return [] + } + #endif var result: [OngoingCallConnectionDescriptionWebrtc] = [] if !reflector.ip.isEmpty { result.append(OngoingCallConnectionDescriptionWebrtc(reflectorId: id, hasStun: false, hasTurn: true, hasTcp: reflector.isTcp, ip: reflector.ip, port: reflector.port, username: "reflector", password: hexString(reflector.peerTag))) @@ -88,6 +93,11 @@ private func callConnectionDescriptionsWebrtc(_ connection: CallSessionConnectio } return result case let .webRtcReflector(reflector): + #if DEBUG + if "".isEmpty { + return [] + } + #endif var result: [OngoingCallConnectionDescriptionWebrtc] = [] if !reflector.ip.isEmpty { result.append(OngoingCallConnectionDescriptionWebrtc(reflectorId: 0, hasStun: reflector.hasStun, hasTurn: reflector.hasTurn, hasTcp: false, ip: reflector.ip, port: reflector.port, username: reflector.username, password: reflector.password)) @@ -1192,6 +1202,31 @@ public final class OngoingCallContext { return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart } + public func video(isIncoming: Bool) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + + queue.async { + guard let strongSelf = self else { + return + } + strongSelf.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + let innerDisposable = context.addVideoOutput(withIsIncoming: isIncoming, sink: { videoFrameData in + subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData)) + }) + disposable.set(ActionDisposable { + innerDisposable.dispose() + }) + } + } + } + + return disposable + } + } + public func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { self.withContext { context in if let context = context as? OngoingCallThreadLocalContextWebrtc { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift index ea2b6a8360f..fb879ae51fc 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift @@ -630,7 +630,7 @@ private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategory } public struct PeerChannelMemberCategoryControl { - fileprivate let key: PeerChannelMemberContextKey + let key: PeerChannelMemberContextKey } private final class PeerChannelMemberContextWithSubscribers { diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 32a057e6989..c410435c02a 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -147,6 +147,12 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { } } + func reset(peerId: PeerId, control: PeerChannelMemberCategoryControl) { + if let context = self.contexts[peerId] { + context.reset(control.key) + } + } + func profileData(postbox: Postbox, network: Network, peerId: PeerId, customData: Signal?) -> Disposable { let context: ProfileDataPreloadContext if let current = self.profileDataPreloadContexts[peerId] { @@ -285,6 +291,12 @@ public final class PeerChannelMemberCategoriesContextsManager { } } + public func reset(peerId: PeerId, control: PeerChannelMemberCategoryControl) { + self.impl.with { impl in + impl.reset(peerId: peerId, control: control) + } + } + private func getContext(engine: TelegramEngine, postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { assert(Queue.mainQueue().isCurrent()) let (disposable, control) = self.impl.syncWith({ impl in diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index c2707160b81..711273b0103 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -192,11 +192,19 @@ public final class ChatTextInputTextUrlAttribute: NSObject { } } -public final class ChatTextInputTextCustomEmojiAttribute: NSObject { +public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { + private enum CodingKeys: String, CodingKey { + case interactivelySelectedFromPackId + case fileId + case file + case topicId + case topicInfo + } + public let interactivelySelectedFromPackId: ItemCollectionId? public let fileId: Int64 - public let topicInfo: (Int64, EngineMessageHistoryThread.Info)? public let file: TelegramMediaFile? + public let topicInfo: (Int64, EngineMessageHistoryThread.Info)? public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, topicInfo: (Int64, EngineMessageHistoryThread.Info)? = nil) { self.interactivelySelectedFromPackId = interactivelySelectedFromPackId @@ -207,6 +215,29 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject { super.init() } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.interactivelySelectedFromPackId = try container.decodeIfPresent(ItemCollectionId.self, forKey: .interactivelySelectedFromPackId) + self.fileId = try container.decode(Int64.self, forKey: .fileId) + self.file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) + if let topicId = try container.decodeIfPresent(Int64.self, forKey: .topicId), let topicInfo = try container.decodeIfPresent(EngineMessageHistoryThread.Info.self, forKey: .topicInfo) { + self.topicInfo = (topicId, topicInfo) + } else { + self.topicInfo = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.interactivelySelectedFromPackId, forKey: .interactivelySelectedFromPackId) + try container.encode(self.fileId, forKey: .fileId) + try container.encodeIfPresent(self.file, forKey: .file) + if let (topicId, topicInfo) = self.topicInfo { + try container.encode(topicId, forKey: .topicId) + try container.encode(topicInfo, forKey: .topicInfo) + } + } + override public func isEqual(_ object: Any?) -> Bool { if let other = object as? ChatTextInputTextCustomEmojiAttribute { return self === other diff --git a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h index 13bf1d43033..734f6001617 100644 --- a/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h +++ b/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h @@ -261,6 +261,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { - (void)setIsMuted:(bool)isMuted; - (void)setIsLowBatteryLevel:(bool)isLowBatteryLevel; - (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType; +- (GroupCallDisposable * _Nonnull)addVideoOutputWithIsIncoming:(bool)isIncoming sink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink; - (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion; - (void)requestVideo:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer; - (void)setRequestedVideoAspect:(float)aspect; diff --git a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm index 3795954fc46..907e01cd87c 100644 --- a/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm +++ b/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm @@ -115,7 +115,9 @@ virtual void start() override { if (!_audioDeviceModule->Playing()) { _audioDeviceModule->InitPlayout(); //_audioDeviceModule->InitRecording(); - _audioDeviceModule->InternalStartPlayout(); + if (_audioDeviceModule->PlayoutIsInitialized()) { + _audioDeviceModule->InternalStartPlayout(); + } //_audioDeviceModule->InternalStartRecording(); } } @@ -850,6 +852,9 @@ @interface OngoingCallThreadLocalContextWebrtc () { bool _useManualAudioSessionControl; SharedCallAudioDevice *_audioDevice; + int _nextSinkId; + NSMutableDictionary *_sinks; + rtc::scoped_refptr _currentAudioDeviceModule; rtc::Thread *_currentAudioDeviceModuleThread; @@ -1030,6 +1035,8 @@ - (instancetype _Nonnull)initWithVersion:(NSString * _Nonnull)version queue:(id< _audioDevice = audioDevice; + _sinks = [[NSMutableDictionary alloc] init]; + _useManualAudioSessionControl = true; [RTCAudioSession sharedInstance].useManualAudio = true; @@ -1469,6 +1476,33 @@ - (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType { } } +- (GroupCallDisposable * _Nonnull)addVideoOutputWithIsIncoming:(bool)isIncoming sink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink { + int sinkId = _nextSinkId; + _nextSinkId += 1; + + GroupCallVideoSink *storedSink = [[GroupCallVideoSink alloc] initWithSink:sink]; + _sinks[@(sinkId)] = storedSink; + + if (_tgVoip) { + if (isIncoming) { + _tgVoip->setIncomingVideoOutput([storedSink sink]); + } + } + + __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; + id queue = _queue; + return [[GroupCallDisposable alloc] initWithBlock:^{ + [queue dispatch:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + [strongSelf->_sinks removeObjectForKey:@(sinkId)]; + }]; + }]; +} + - (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion { if (_tgVoip) { __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index d386c496314..2f773d98bdf 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit d386c496314966eabd363452a90bf9a819fcb293 +Subproject commit 2f773d98bdfd60d55247b2b3adb8ec70a1fc5748 diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index be860fd19c9..6965fff4309 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -55,6 +55,7 @@ - (CGFloat)valueAt:(CGFloat)t { springAnimation.damping = 500.0f; springAnimation.duration = 0.5; springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + return springAnimation; } diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 28bbb9cbd81..c4f8086c23f 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -116,6 +116,9 @@ - (void)_65087dc8_setPreferredFrameRateRange:(CAFrameRateRange)range API_AVAILAB @implementation UIScrollView (FrameRateRangeOverride) - (void)fixScrollDisplayLink { + if (@available(iOS 16.0, *)) { + return; + } static NSString *scrollHeartbeatKey = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -176,7 +179,8 @@ + (void)load [RuntimeUtils swizzleInstanceMethodOfClass:[UIWindow class] currentSelector:@selector(initWithFrame:) newSelector:@selector(_65087dc8_initWithFrame:)]; - if (@available(iOS 15.0, *)) { + if (@available(iOS 16.0, *)) { + } else if (@available(iOS 15.0, *)) { [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; } }); diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 10868dea54e..da49acb4a58 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -22,7 +22,7 @@ public enum UndoOverlayContent { case chatRemovedFromFolder(chatTitle: String, folderTitle: String) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case setProximityAlert(title: String, text: String, cancelled: Bool) - case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, text: String, action: String?) + case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, text: String, action: String?, duration: Double) case linkCopied(text: String) case banned(text: String) case importedMessage(text: String) @@ -39,7 +39,7 @@ public enum UndoOverlayContent { case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) case inviteRequestSent(title: String, text: String) - case image(image: UIImage, title: String?, text: String, undo: Bool) + case image(image: UIImage, title: String?, text: String, round: Bool, undo: Bool) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index f1714680e05..361ca0493a4 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -384,7 +384,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { thumbnailItem = .animated(EngineMediaResource(item.file.resource), item.file.dimensions ?? PixelDimensions(width: 512, height: 512), item.file.isVideoSticker) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource) } else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) } } @@ -407,7 +407,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource._asResource(), animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resourceReference) + updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resourceReference) |> mapError { _ -> EngineMediaResource.Fetch.Error in return .generic } @@ -518,7 +518,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .invitedToVoiceChat(context, peer, text, action): + case let .invitedToVoiceChat(context, peer, text, action, duration): self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) self.iconNode = nil self.iconCheckNode = nil @@ -536,11 +536,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if let action = action { displayUndo = true undoText = action - self.originalRemainingSeconds = 5 } else { displayUndo = false - self.originalRemainingSeconds = 3 } + self.originalRemainingSeconds = duration case let .audioRate(slowdown, text): self.avatarNode = nil self.iconNode = nil @@ -668,7 +667,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { thumbnailItem = .animated(EngineMediaResource(file.resource)) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: file.resource) } else if let dimensions = file.dimensions, let resource = chatMessageStickerResource(file: file, small: true) as? TelegramMediaResource { - thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: resource) } @@ -690,7 +689,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource._asResource(), animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resourceReference) + updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resourceReference) |> mapError { _ -> EngineMediaResource.Fetch.Error in return .generic } @@ -861,13 +860,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { displayUndo = false } - case let .image(image, title, text, undo): + case let .image(image, title, text, round, undo): self.avatarNode = nil self.iconNode = ASImageNode() self.iconNode?.clipsToBounds = true self.iconNode?.contentMode = .scaleAspectFill self.iconNode?.image = image - self.iconNode?.cornerRadius = 4.0 + self.iconNode?.cornerRadius = round ? 16.0 : 4.0 self.iconImageSize = CGSize(width: 32.0, height: 32.0) self.iconCheckNode = nil self.animationNode = nil @@ -1101,7 +1100,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.content = content switch content { - case let .image(image, title, text, _): + case let .image(image, title, text, _, _): self.iconNode?.image = image if let title = title { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) diff --git a/submodules/Utils/DarwinDirStat/BUILD b/submodules/Utils/DarwinDirStat/BUILD new file mode 100644 index 00000000000..1505fd502e7 --- /dev/null +++ b/submodules/Utils/DarwinDirStat/BUILD @@ -0,0 +1,21 @@ + +objc_library( + name = "DarwinDirStat", + enable_modules = True, + module_name = "DarwinDirStat", + srcs = glob([ + "Sources/*.m", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Utils/DarwinDirStat/Package.swift b/submodules/Utils/DarwinDirStat/Package.swift new file mode 100644 index 00000000000..4011c1c65b2 --- /dev/null +++ b/submodules/Utils/DarwinDirStat/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "DarwinDirStat", + platforms: [.macOS(.v10_12)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "DarwinDirStat", + targets: ["DarwinDirStat"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "DarwinDirStat", + dependencies: [], + path: ".", + exclude: ["BUILD"], + publicHeadersPath: "PublicHeaders", + cSettings: [ + .headerSearchPath("PublicHeaders") + ]), + ] +) diff --git a/submodules/Utils/DarwinDirStat/PublicHeaders/DarwinDirStat/DarwinDirStat.h b/submodules/Utils/DarwinDirStat/PublicHeaders/DarwinDirStat/DarwinDirStat.h new file mode 100644 index 00000000000..6df75730b7d --- /dev/null +++ b/submodules/Utils/DarwinDirStat/PublicHeaders/DarwinDirStat/DarwinDirStat.h @@ -0,0 +1,14 @@ +#ifndef DarwinDirStat_h +#define DarwinDirStat_h + +#import + +struct darwin_dirstat { + off_t total_size; + uint64_t descendants; +}; + +int dirstat_np(const char *path, int flags, struct darwin_dirstat *ds, size_t dirstat_size); + + +#endif diff --git a/submodules/Utils/DarwinDirStat/Sources/DarwinDirStat.m b/submodules/Utils/DarwinDirStat/Sources/DarwinDirStat.m new file mode 100644 index 00000000000..1c7866bdaf3 --- /dev/null +++ b/submodules/Utils/DarwinDirStat/Sources/DarwinDirStat.m @@ -0,0 +1,2 @@ +#import + diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index f9f4928b4bc..ed96b11a3bb 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -951,7 +951,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: reference(for: EngineMediaResource(representation.resource), media: EngineMedia(file.file)))) } let dimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000) - convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: reference(for: EngineMediaResource(file.file.resource), media: EngineMedia(file.file)))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: reference(for: EngineMediaResource(file.file.resource), media: EngineMedia(file.file)))) let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) self.patternImageDisposable.set((signal @@ -1619,7 +1619,7 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun switch spec { case let .image(representation, _, _): - self.fetchDisposable = (fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: representation.resource)) + self.fetchDisposable = (fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.standalone(resource: representation.resource)) |> deliverOnMainQueue).start() self.dataDisposable = (context.account.postbox.mediaBox.resourceData(representation.resource) |> deliverOnMainQueue).start(next: { [weak self] dataValue in @@ -1803,7 +1803,7 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun gradientSpec = WallpaperGradiendComponentView.Spec(colors: file.settings.colors) } if let dimensions = file.file.dimensions { - let representation = TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: file.file.immediateThumbnailData, hasVideo: false) + let representation = TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: file.file.immediateThumbnailData, hasVideo: false, isPersonal: false) imageSpec = WallpaperImageComponentView.Spec.image(representation: representation, isPattern: file.isPattern, intensity: CGFloat(file.settings.intensity ?? 100) / 100.0) } } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 90b2e839232..e48af8b4636 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -88,9 +88,9 @@ public func wallpaperDatas(account: Account, accountManager: AccountManager - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: representations[smallestIndex].reference) - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: representations[largestIndex].reference) let thumbnailData: Signal @@ -395,7 +395,7 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData, true)) } else { - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: targetRepresentation.reference) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: targetRepresentation.reference) let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() @@ -907,7 +907,7 @@ public func telegramThemeData(account: Account, accountManager: AccountManager map { data -> Data? in return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil @@ -1117,7 +1117,7 @@ public func themeImage(account: Account, accountManager: AccountManager if let previewRepresentation = previewRepresentation { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(previewRepresentation.resource)) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: fileReference.resourceReference(previewRepresentation.resource)) } else { fetchedThumbnail = .complete() } @@ -1148,7 +1148,7 @@ public func themeImage(account: Account, accountManager: AccountManager if isSupportedTheme { fullSizeData = Signal { subscriber in - let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: reference).start() + let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: reference).start() let disposable = (account.postbox.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) |> map { data -> Data? in return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil @@ -1194,7 +1194,7 @@ public func themeImage(account: Account, accountManager: AccountManager mapToSignal { wallpaper -> Signal<(PresentationTheme?, WallpaperImage?, Data?), NoError> in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { var convertedRepresentations: [ImageRepresentationWithReference] = [] - convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) + convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))) return wallpaperDatas(account: account, accountManager: accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false) |> mapToSignal { _, fullSizeData, complete -> Signal<(PresentationTheme?, WallpaperImage?, Data?), NoError> in guard complete, let fullSizeData = fullSizeData else { @@ -1448,7 +1448,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager mapToSignal { thumbnailData, fullSizeData, complete -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in guard complete, let fullSizeData = fullSizeData else { @@ -1775,7 +1775,7 @@ public func wallpaperThumbnail(account: Account, accountManager: AccountManager< } } }) - let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnail.resource)).start() + let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: fileReference.resourceReference(thumbnail.resource)).start() return ActionDisposable { data.dispose() diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 8b8edc4cf57..34185fc23bd 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -476,8 +476,8 @@ final class WatchMediaHandler: WatchRequestHandler { if let dimensions = media.dimensions { size = dimensions.cgSize } - self.disposable.add(freeMediaFileInteractiveFetched(account: context.account, fileReference: fileReference).start()) - return chatMessageSticker(account: context.account, file: media, small: false, fetched: true, onlyFullSize: true) + self.disposable.add(freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).start()) + return chatMessageSticker(account: context.account, userLocation: .other, file: media, small: false, fetched: true, onlyFullSize: true) } return .complete() } @@ -545,13 +545,13 @@ final class WatchMediaHandler: WatchRequestHandler { } if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { - imageSignal = chatMessagePhotoThumbnail(account: context.account, photoReference: imageReference, onlyFullSize: true) + imageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .other, photoReference: imageReference, onlyFullSize: true) } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { - imageSignal = chatMessageVideoThumbnail(account: context.account, fileReference: fileReference) + imageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .other, fileReference: fileReference) roundVideo = fileReference.media.isInstantVideo } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { - imageSignal = chatWebpageSnippetFile(account: context.account, mediaReference: fileReference.abstract, representation: iconImageRepresentation) + imageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .other, mediaReference: fileReference.abstract, representation: iconImageRepresentation) } } } diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift index 52bff87dd4c..7032cf6f5e2 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift @@ -37,13 +37,14 @@ func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTh legacyController.bind(controller: controller) controller.editingContext = TGMediaEditingContext() - controller.didFinishEditing = { [weak controller] _, result, _, hasChanges in + controller.didFinishEditing = { [weak controller] _, result, _, hasChanges, commit in if !hasChanges { return } if let result = result { completed(result) } + commit?() controller?.dismiss(animated: true) } controller.requestThumbnailImage = { _ -> SSignal in diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 835ab580f9a..50088460bb0 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -260,11 +260,11 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) + thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) |> mapToSignal { value -> Signal in let thumbnailData = value._0 if let data = thumbnailData, let image = UIImage(data: data) { @@ -273,7 +273,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW return .complete() } } - originalSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) + originalSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) |> mapToSignal { value -> Signal in let thumbnailData = value._0 let fullSizeData = value._1 @@ -312,7 +312,7 @@ private func galleryItems(account: Account, results: [ChatContextResult], curren return (galleryItems, focusItem) } -func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: (ViewController, Any?) -> Void) { +func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: (ViewController, Any?) -> Void) { let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style @@ -331,17 +331,6 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, t paintStickersContext.captionPanelView = { return getCaptionPanelView() } - paintStickersContext.presentStickersController = { completion in - if let presentStickers = presentStickers { - return presentStickers({ file, animated, view, rect in - let coder = PostboxEncoder() - coder.encodeRootObject(file) - completion?(coder.makeData(), animated, view, rect) - }) - } else { - return nil - } - } let controller = TGModernGalleryController(context: legacyController.context)! controller.asyncTransitionIn = true @@ -432,16 +421,7 @@ public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionCon for result in results { let editableItem = LegacyWebSearchItem(result: result) if let adjustments = editingState.adjustments(for: editableItem) { - var animated = false - if let entities = adjustments.paintingData?.entities { - for entity in entities { - if let paintEntity = entity as? TGPhotoPaintEntity, paintEntity.animated { - animated = true - break - } - } - } - + let animated = adjustments.paintingData?.hasAnimation ?? false if let imageSignal = editingState.imageSignal(for: editableItem) { let signal = imageSignal.map { image -> Any in if let image = image as? UIImage { diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 79695f3c85a..96f8cd11343 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -10,51 +10,6 @@ import TelegramPresentationData import AccountContext import AttachmentUI -public func requestContextResults(context: AccountContext, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal { - return context.engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) - |> `catch` { error -> Signal in - return .single(nil) - } - |> mapToSignal { resultsStruct -> Signal in - let results = resultsStruct?.results - - var collection = existingResults - var updated: Bool = false - if let existingResults = existingResults, let results = results { - var newResults: [ChatContextResult] = [] - var existingIds = Set() - for result in existingResults.results { - newResults.append(result) - existingIds.insert(result.id) - } - for result in results.results { - if !existingIds.contains(result.id) { - newResults.append(result) - existingIds.insert(result.id) - updated = true - } - } - collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, results: newResults, cacheTimeout: existingResults.cacheTimeout) - } else { - collection = results - updated = true - } - if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated { - let nextResults = requestContextResults(context: context, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit) - if collection.results.count > 10 { - return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false)) - |> then(nextResults) - } else { - return nextResults - } - } else if let collection = collection { - return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false)) - } else { - return .single(nil) - } - } -} - public enum WebSearchMode { case media case avatar @@ -152,12 +107,6 @@ public final class WebSearchController: ViewController { private var navigationContentNode: WebSearchNavigationContentNode? - public var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)? { - didSet { - self.controllerNode.presentStickers = self.presentStickers - } - } - public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } { didSet { self.controllerNode.getCaptionPanelView = self.getCaptionPanelView @@ -497,7 +446,7 @@ public final class WebSearchController: ViewController { } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let results = requestContextResults(context: context, botId: user.id, query: query, peerId: peerId, limit: 64) + let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: peerId, limit: 64) |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in return .contextRequestResult(.user(user), results?.results) diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift index 71408173ebf..fc840cb6913 100644 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift @@ -184,7 +184,6 @@ class WebSearchControllerNode: ASDisplayNode { var cancel: (() -> Void)? var dismissInput: (() -> Void)? - var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)? var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } init(controller: WebSearchController, context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) { @@ -744,7 +743,7 @@ class WebSearchControllerNode: ASDisplayNode { strongSelf.controllerInteraction.sendSelected(result, false, nil) strongSelf.cancel?() } - }, presentStickers: self.presentStickers, getCaptionPanelView: self.getCaptionPanelView, present: present) + }, getCaptionPanelView: self.getCaptionPanelView, present: present) } } else { if let results = self.currentProcessedResults?.results { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index db9156e61b5..cf0a43089e4 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -37,12 +37,12 @@ struct WebSearchGalleryEntry: Equatable { switch self.result { case let .externalReference(externalReference): if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions { - let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } case let .internalReference(internalReference): if let file = internalReference.file { - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) + return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true), controllerInteraction: controllerInteraction) } } preconditionFailure() diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift index b1070ca6d75..21d75066137 100644 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchItem.swift @@ -130,14 +130,14 @@ final class WebSearchItemNode: GridItemNode { var representations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false)) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } if !representations.isEmpty { let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - updateImageSignal = mediaGridMessagePhoto(account: item.account, photoReference: .standalone(media: tmpImage)) + updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage)) } else { updateImageSignal = .complete() } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index b8b9bbadc31..8f10d3c0a45 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/BotPaymentsUI:BotPaymentsUI", "//submodules/PromptUI:PromptUI", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/QrCodeUI:QrCodeUI", + "//submodules/InstantPageUI:InstantPageUI", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppAlertContentNode.swift b/submodules/WebUI/Sources/WebAppAlertContentNode.swift index 652904f5ae3..d408b8f675f 100644 --- a/submodules/WebUI/Sources/WebAppAlertContentNode.swift +++ b/submodules/WebUI/Sources/WebAppAlertContentNode.swift @@ -10,6 +10,16 @@ import TelegramUIPreferences import AccountContext import AppBundle import PhotoResources +import CheckNode +import Markdown + +private let textFont = Font.regular(13.0) +private let boldTextFont = Font.semibold(13.0) + +private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +} + private final class WebAppAlertContentNode: AlertContentNode { private let strings: PresentationStrings @@ -20,6 +30,9 @@ private final class WebAppAlertContentNode: AlertContentNode { private let appIconNode: ASImageNode private let iconNode: ASImageNode + private let allowWriteCheckNode: InteractiveCheckNode + private let allowWriteLabelNode: ASTextNode + private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] @@ -32,7 +45,13 @@ private final class WebAppAlertContentNode: AlertContentNode { return self.isUserInteractionEnabled } - init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], actions: [TextAlertAction]) { + var allowWriteAccess: Bool = true { + didSet { + self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true) + } + } + + init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, actions: [TextAlertAction]) { self.strings = strings self.peerName = peerName @@ -54,6 +73,12 @@ private final class WebAppAlertContentNode: AlertContentNode { self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true + + self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) + self.allowWriteCheckNode.setSelected(true, animated: false) + self.allowWriteLabelNode = ASTextNode() + self.allowWriteLabelNode.maximumNumberOfLines = 4 + self.allowWriteLabelNode.isUserInteractionEnabled = true self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true @@ -77,6 +102,12 @@ private final class WebAppAlertContentNode: AlertContentNode { self.addSubnode(self.textNode) self.addSubnode(self.appIconNode) self.addSubnode(self.iconNode) + + if requestWriteAccess { + self.addSubnode(self.allowWriteCheckNode) + self.addSubnode(self.allowWriteLabelNode) + } + self.addSubnode(self.actionNodesSeparator) @@ -88,10 +119,16 @@ private final class WebAppAlertContentNode: AlertContentNode { self.addSubnode(separatorNode) } + self.allowWriteCheckNode.valueChanged = { [weak self] value in + if let strongSelf = self { + strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess + } + } + self.updateTheme(theme) if let peerIcon = self.peerIcon { - let _ = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: peerIcon)).start() + let _ = freeMediaFileInteractiveFetched(account: account, userLocation: .other, fileReference: .standalone(media: peerIcon)).start() self.iconDisposable = (svgIconImageFile(account: account, fileReference: .standalone(media: peerIcon)) |> deliverOnMainQueue).start(next: { [weak self] transform in if let strongSelf = self { @@ -109,12 +146,26 @@ private final class WebAppAlertContentNode: AlertContentNode { self.iconDisposable?.dispose() } + override func didLoad() { + super.didLoad() + + self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) + } + + @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.allowWriteCheckNode.isUserInteractionEnabled { + self.allowWriteAccess = !self.allowWriteAccess + } + } + override func updateTheme(_ theme: AlertControllerTheme) { self.textNode.attributedText = NSAttributedString(string: strings.WebApp_AddToAttachmentText(self.peerName).string, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.appIconNode.image = generateTintedImage(image: self.appIconNode.image, color: theme.accentColor) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/BotPlus"), color: theme.accentColor) + self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peerName).string, color: theme.primaryColor) + self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { actionNode.updateTheme(theme) @@ -146,6 +197,23 @@ private final class WebAppAlertContentNode: AlertContentNode { let textSize = self.textNode.measure(size) var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize) + origin.y += textSize.height + + var entriesHeight: CGFloat = 0.0 + + if self.allowWriteLabelNode.supernode != nil { + origin.y += 16.0 + entriesHeight += 16.0 + + let checkSize = CGSize(width: 22.0, height: 22.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + + let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) + transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) + transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize)) + origin.y += allowWriteSize.height + entriesHeight += allowWriteSize.height + } let actionButtonHeight: CGFloat = 44.0 var minActionsWidth: CGFloat = 0.0 @@ -180,7 +248,7 @@ private final class WebAppAlertContentNode: AlertContentNode { } let resultWidth = contentWidth + insets.left + insets.right - let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 17.0 + insets.top + insets.bottom) + let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + entriesHeight + actionsHeight + 17.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) @@ -227,7 +295,7 @@ private final class WebAppAlertContentNode: AlertContentNode { nodeIndex += 1 } - iconFrame.origin.x = floorToScreenPixels((resultSize.width - iconFrame.width) / 2.0) + 19.0 + iconFrame.origin.x = floorToScreenPixels((resultSize.width - iconFrame.width) / 2.0) + 21.0 transition.updateFrame(node: self.appIconNode, frame: CGRect(x: iconFrame.minX - 50.0, y: iconFrame.minY + 3.0, width: 42.0, height: 42.0)) transition.updateFrame(node: self.iconNode, frame: iconFrame) @@ -239,7 +307,7 @@ private final class WebAppAlertContentNode: AlertContentNode { } } -public func addWebAppToAttachmentController(context: AccountContext, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], completion: @escaping () -> Void) -> AlertController { +public func addWebAppToAttachmentController(context: AccountContext, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, completion: @escaping (Bool) -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme let strings = presentationData.strings @@ -251,10 +319,10 @@ public func addWebAppToAttachmentController(context: AccountContext, peerName: S }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_AddToAttachmentAdd, action: { dismissImpl?(true) - completion() + completion(true) })] - contentNode = WebAppAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peerName: peerName, icons: icons, actions: actions) + contentNode = WebAppAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peerName: peerName, icons: icons, requestWriteAccess: requestWriteAccess, actions: actions) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 18d172acb66..7d7b62fcea4 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -21,6 +21,8 @@ import MoreButtonNode import BotPaymentsUI import PromptUI import PhoneNumberFormat +import QrCodeUI +import InstantPageUI private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -306,7 +308,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } if let fileReference = fileReference { - let _ = freeMediaFileInteractiveFetched(account: strongSelf.context.account, fileReference: fileReference).start() + let _ = freeMediaFileInteractiveFetched(account: strongSelf.context.account, userLocation: .other, fileReference: fileReference).start() } strongSelf.iconDisposable = (svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder) |> deliverOnMainQueue).start(next: { [weak self] transform in @@ -591,6 +593,8 @@ public final class WebAppController: ViewController, AttachmentContainable { private let hapticFeedback = HapticFeedback() + private weak var currentQrCodeScannerScreen: QrCodeScanScreen? + private var delayedScriptMessage: WKScriptMessage? private func handleScriptMessage(_ message: WKScriptMessage) { guard let controller = self.controller else { @@ -680,10 +684,27 @@ public final class WebAppController: ViewController, AttachmentContainable { } case "web_app_open_link": if let json = json, let url = json["url"] as? String { + let tryInstantView = json["try_instant_view"] as? Bool ?? false let currentTimestamp = CACurrentMediaTime() if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { self.webView?.lastTouchTimestamp = nil - self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + if tryInstantView { + let _ = (resolveInstantViewUrl(account: self.context.account, url: url) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case let .instantView(webPage, anchor): + let controller = InstantPageController(context: strongSelf.context, webPage: webPage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate), anchor: anchor) + strongSelf.controller?.getNavigationController()?.pushViewController(controller) + default: + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + } + }) + } else { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + } } } case "web_app_setup_back_button": @@ -799,8 +820,38 @@ public final class WebAppController: ViewController, AttachmentContainable { if let json = json, let needConfirmation = json["need_confirmation"] as? Bool { self.needDismissConfirmation = needConfirmation } - case "web_app_request_phone": - break + case "web_app_open_scan_qr_popup": + var info: String = "" + if let json = json, let text = json["text"] as? String { + info = text + } + let controller = QrCodeScanScreen(context: self.context, subject: .custom(info: info)) + controller.completion = { [weak self] result in + if let strongSelf = self { + if let result = result { + strongSelf.sendQrCodeScannedEvent(data: result) + } else { + strongSelf.sendQrCodeScannerClosedEvent() + } + } + } + self.currentQrCodeScannerScreen = controller + self.controller?.present(controller, in: .window(.root)) + case "web_app_close_scan_qr_popup": + if let controller = self.currentQrCodeScannerScreen { + self.currentQrCodeScannerScreen = nil + controller.dismissAnimated() + } + case "web_app_read_text_from_clipboard": + if let json = json, let requestId = json["req_id"] as? String { + let currentTimestamp = CACurrentMediaTime() + var fillData = false + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0, self.controller?.url == nil { + self.webView?.lastTouchTimestamp = nil + fillData = true + } + self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) + } default: break } @@ -930,6 +981,26 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.webView?.sendEvent(name: "phone_requested", data: paramsString) } + + fileprivate func sendQrCodeScannedEvent(data: String?) { + let paramsString = data.flatMap { "{data: \"\($0)\"}" } ?? "{}" + self.webView?.sendEvent(name: "qr_text_received", data: paramsString) + } + + fileprivate func sendQrCodeScannerClosedEvent() { + self.webView?.sendEvent(name: "scan_qr_popup_closed", data: nil) + } + + fileprivate func sendClipboardTextEvent(requestId: String, fillData: Bool) { + var paramsString: String + if fillData { + let data = UIPasteboard.general.string ?? "" + paramsString = "{req_id: \"\(requestId)\", data: \"\(data)\"}" + } else { + paramsString = "{req_id: \"\(requestId)\"}" + } + self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString) + } } fileprivate var controllerNode: Node { @@ -1067,7 +1138,7 @@ public final class WebAppController: ViewController, AttachmentContainable { let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId}) - if self?.url == nil, let attachMenuBot = attachMenuBot, attachMenuBot.hasSettings { + if self?.url == nil, let attachMenuBot = attachMenuBot, attachMenuBot.flags.contains(.hasSettings) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in diff --git a/third-party/webrtc/webrtc b/third-party/webrtc/webrtc index 330ee2c9f7a..9f535bc11e3 160000 --- a/third-party/webrtc/webrtc +++ b/third-party/webrtc/webrtc @@ -1 +1 @@ -Subproject commit 330ee2c9f7a33d443be72de55f429faf33e846dd +Subproject commit 9f535bc11e3fd19909eb3de2f5c0836c1e3f83f4 diff --git a/versions.json b/versions.json index 22a62e11845..d09264bb06a 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.1.7", + "app": "1.1.8", "bazel": "5.3.1", "xcode": "14.1" }