From 3e6363abf9c25af916dd75c574bc317eccecf978 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 May 2026 16:50:05 +0200 Subject: [PATCH] Various improvements --- Telegram/BUILD | 3 + .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/AccountContext.swift | 18 +- .../Sources/ChatController.swift | 17 + submodules/AdUI/Sources/AdInfoScreen.swift | 2 +- .../Source/ASEditableTextNode.mm | 2 + .../Sources/AttachmentPanel.swift | 21 +- submodules/BotPaymentsUI/BUILD | 11 + .../Sources/BotCheckoutActionButton.swift | 1 + .../Sources/BotCheckoutController.swift | 5 +- .../Sources/BotCheckoutControllerNode.swift | 8 +- .../Sources/BotCheckoutHeaderItem.swift | 2 +- .../Sources/BotCheckoutInfoController.swift | 12 +- .../BotCheckoutInfoControllerNode.swift | 2 +- ...BotCheckoutNativeCardEntryController.swift | 12 +- ...heckoutNativeCardEntryControllerNode.swift | 2 +- .../BotCheckoutPasswordEntryController.swift | 436 +---- .../BotCheckoutPaymentMethodScreen.swift | 536 +++++ .../BotCheckoutPaymentMethodSheet.swift | 262 --- ...PaymentShippingOptionSheetController.swift | 224 --- .../BotCheckoutShippingOptionScreen.swift | 418 ++++ .../Sources/BrowserBookmarksScreen.swift | 8 +- .../Sources/BrowserInstantPageContent.swift | 5 +- .../Sources/BrowserReadability.swift | 106 +- .../Sources/CalendarMessageScreen.swift | 6 +- .../Sources/CallListController.swift | 8 +- .../Sources/CallListControllerNode.swift | 2 +- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 2 +- .../Sources/ChatListControllerNode.swift | 135 +- .../ChatListFilterPresetController.swift | 5 + .../ChatListFilterPresetListController.swift | 5 + .../ChatListFilterPresetListItem.swift | 4 +- ...hatListFilterPresetListSuggestedItem.swift | 10 +- .../Sources/ChatListSearchListPaneNode.swift | 6 +- .../Node/ChatListArchiveInfoItem.swift | 1 + .../Sources/Node/ChatListItem.swift | 106 +- .../Source/Components/Image.swift | 14 +- .../Sources/BundleIconComponent.swift | 11 +- .../ReactionListContextMenuContent.swift | 2 + .../Sources/DeviceLocationManager.swift | 16 +- .../Source/ActionSheetControllerNode.swift | 6 + .../Source/ActionSheetItemGroupNode.swift | 1 + .../Display/Source/CAAnimationUtils.swift | 21 +- .../ContainedViewLayoutTransition.swift | 10 +- submodules/Display/Source/ListView.swift | 1 + .../Navigation/NavigationController.swift | 57 +- .../Navigation/NavigationModalContainer.swift | 1 + .../Navigation/NavigationSplitContainer.swift | 29 +- .../Display/Source/ScrollToTopProxyView.swift | 84 +- submodules/DrawingUI/BUILD | 4 + .../DrawingUI/Sources/ColorPickerScreen.swift | 90 +- .../DrawingUI/Sources/DrawingScreen.swift | 688 +++---- .../Sources/ModeAndSizeComponent.swift | 576 ++++-- .../ChatItemGalleryFooterContentNode.swift | 11 +- .../GalleryThumbnailContainerNode.swift | 1 + .../Items/UniversalVideoGalleryItem.swift | 9 +- .../ImportStickerPackControllerNode.swift | 1 + .../Sources/InstantPageControllerNode.swift | 5 +- .../InstantPageReferenceControllerNode.swift | 1 + .../Sources/ItemListStickerPackItem.swift | 9 +- submodules/ItemListUI/BUILD | 1 + .../Sources/ItemListController.swift | 10 + .../Sources/ItemListControllerNode.swift | 203 +- .../ItemListUI/Sources/ItemListItem.swift | 2 +- .../Sources/ItemListRevealOptionsNode.swift | 240 ++- .../Items/ItemListDisclosureItem.swift | 32 +- .../Sources/Items/ItemListEditableItem.swift | 30 +- .../Sources/Items/ItemListInfoItem.swift | 3 + .../Sources/Items/ItemListSwitchItem.swift | 20 +- .../JoinLinkPreviewControllerNode.swift | 1 + .../JoinLinkPreviewPeerContentNode.swift | 1 + .../LanguageLinkPreviewControllerNode.swift | 1 + .../TGMediaPickerGalleryInterfaceView.h | 2 +- .../TGPhotoPaintStickersContext.h | 3 + .../LegacyComponents/TGPhotoToolbarView.h | 40 +- .../TGPhotoToolbarViewProtocol.h | 83 + .../Sources/TGCameraController.m | 17 +- .../TGMediaPickerGalleryInterfaceView.m | 99 +- .../Sources/TGMediaPickerModernGalleryMixin.m | 15 +- .../Sources/TGPhotoCropController.h | 1 + .../Sources/TGPhotoCropController.m | 44 +- .../Sources/TGPhotoEditorBlurToolView.m | 20 +- .../Sources/TGPhotoEditorController.m | 71 +- .../Sources/TGPhotoEditorInterfaceAssets.m | 12 +- .../Sources/TGPhotoToolbarView.m | 29 + .../Sources/TGPhotoVideoEditor.m | 15 +- submodules/LegacyMediaPickerUI/BUILD | 1 + .../Sources/LegacyAttachmentMenu.swift | 110 +- .../Sources/LegacyMediaPickers.swift | 1 + .../Sources/LegacyPaintStickersContext.swift | 108 +- submodules/LegacyUI/BUILD | 1 + .../LegacyUI/Sources/LegacyController.swift | 84 +- .../ChatMessageLiveLocationTimerNode.swift | 12 +- submodules/LocationUI/BUILD | 7 + .../Sources/LocationActionListItem.swift | 63 +- .../Sources/LocationAnnotation.swift | 26 +- .../LocationDistancePickerScreen.swift | 1156 ++++++----- .../Sources/LocationInfoListItem.swift | 273 +-- .../Sources/LocationLiveListItem.swift | 231 +-- .../LocationLiveLocationContextMenu.swift | 68 + .../Sources/LocationMapHeaderNode.swift | 416 +++- .../LocationUI/Sources/LocationMapNode.swift | 66 +- .../Sources/LocationOptionsNode.swift | 64 - .../Sources/LocationPickerController.swift | 69 +- .../LocationPickerControllerNode.swift | 39 +- .../Sources/LocationViewController.swift | 145 +- .../Sources/LocationViewControllerNode.swift | 266 ++- submodules/MediaPickerUI/BUILD | 2 +- .../Sources/LegacyMediaPickerGallery.swift | 40 +- .../MediaGroupsContextMenuContent.swift | 1 + .../Sources/MediaGroupsScreen.swift | 2 +- .../Sources/MediaPickerManageNode.swift | 7 +- .../Sources/MediaPickerPhotoToolbarView.swift | 1187 ++++++++++++ .../Sources/MediaPickerScreen.swift | 121 +- .../Sources/MediaPickerSelectedListNode.swift | 1 + .../Sources/OpenInAppIconResources.swift | 14 +- .../Sources/OpenInOptions.swift | 30 +- ...roller.swift => OpenInOptionsScreen.swift} | 38 +- .../Sources/SecureIdAuthControllerNode.swift | 1 + .../TwoFactorAuthDataInputScreen.swift | 1 + submodules/PeerInfoUI/BUILD | 1 - .../ChannelBannedMemberController.swift | 91 +- .../Sources/ItemListReactionItem.swift | 2 +- .../Sources/SecretChatKeyControllerNode.swift | 2 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 63 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 47 +- .../Sources/PremiumIntroScreen.swift | 63 +- .../Sources/PremiumLimitScreen.swift | 55 +- .../Sources/PremiumPrivacyScreen.swift | 47 +- .../Sources/StickersCarouselComponent.swift | 1 + .../Sources/ReactionContextNode.swift | 1 + submodules/SettingsUI/BUILD | 1 - .../BubbleSettingsController.swift | 1 + .../StorageUsageExceptionsScreen.swift | 9 - .../Data and Storage/WebBrowserItem.swift | 21 +- .../WebBrowserSettingsController.swift | 6 +- .../LocalizationListController.swift | 2 +- .../LocalizationListControllerNode.swift | 2 +- .../Exceptions/NotificationExceptions.swift | 4 +- .../PasscodeOptionsController.swift | 61 +- .../PrivacyAndSecurityController.swift | 72 +- .../PrivacyIntroControllerNode.swift | 2 +- .../ItemListRecentSessionItem.swift | 28 +- .../Recent Sessions/ItemListWebsiteItem.swift | 46 +- .../RecentSessionsController.swift | 66 +- .../RecentSessionsHeaderItem.swift | 8 +- ...actionNotificationSettingsController.swift | 108 +- .../InstalledStickerPacksController.swift | 62 +- .../TermsOfServiceControllerNode.swift | 6 + .../TextSizeSelectionController.swift | 1 + .../Sources/ThemePickerGridItem.swift | 26 +- .../Themes/ThemePreviewController.swift | 10 +- .../Themes/ThemePreviewControllerNode.swift | 4 +- .../Themes/ThemeSettingsAccentColorItem.swift | 2 +- .../Themes/ThemeSettingsAppIconItem.swift | 2 +- .../Sources/ShareControllerNode.swift | 1 + .../Sources/StickerPackPreviewGridItem.swift | 7 +- .../Sources/StickerPackScreen.swift | 126 +- submodules/TelegramApi/Sources/Api41.swift | 17 +- .../MediaNavigationAccessoryHeaderNode.swift | 1 + .../Sources/CallController.swift | 56 +- .../VoiceChatCameraPreviewController.swift | 1 + .../Sources/VoiceChatJoinScreen.swift | 1 + .../VoiceChatRecordingSetupController.swift | 1 + .../Settings/AccountWebBrowserSettings.swift | 46 +- .../TelegramEngine/Messages/BotWebView.swift | 13 + .../TelegramEngine/Peers/SearchPeers.swift | 2 +- .../PresentationResourcesRootController.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 9 +- submodules/TelegramUI/BUILD | 1 - .../AlertWebpagePreviewComponent.swift | 8 +- .../Sources/AttachmentFileSearchItem.swift | 2 +- .../Sources/ButtonComponent.swift | 195 +- .../CameraScreen/Sources/CameraScreen.swift | 5 +- .../Sources/CaptureControlsComponent.swift | 27 +- .../CameraScreen/Sources/ModeComponent.swift | 202 +- .../Sources/ShutterBlobView.swift | 2 +- .../Sources/ChatButtonKeyboardInputNode.swift | 9 +- .../Sources/ChatInputTextNode.swift | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 56 +- .../Sources/ChatMessageItemImpl.swift | 21 + .../Sources/ChatMessageItemView.swift | 6 +- .../ChatMessagePollBubbleContentNode.swift | 16 +- .../ChatMessageSelectionInputPanelNode.swift | 18 +- .../Sources/ChatQrCodeScreen.swift | 1 + .../ChatRecentActionsControllerNode.swift | 6 +- .../ChatSendAsPeerListContextItem.swift | 1 + .../ChatSendAudioMessageContextPreview.swift | 6 +- .../Sources/EditableTokenListNode.swift | 2 + .../Sources/ChatControllerInteraction.swift | 16 +- .../StickerPaneSearchContentNode.swift | 2 +- .../ChatListFilterTabContainerNode.swift | 1 + .../Sources/ChatListNoticeItem.swift | 4 +- .../ChatListSearchFiltersContainerNode.swift | 1 + .../ChatScheduleTimeControllerNode.swift | 1 + .../Components/ChatThemeScreen/BUILD | 10 +- .../Sources/ChatThemeScreen.swift | 1726 ++++++++++------- .../Sources/ComposeTodoScreen.swift | 70 +- .../Sources/PhoneInputItem.swift | 11 +- .../Sources/ContextActionsContainerNode.swift | 1 + .../Sources/ContextControllerImpl.swift | 1 + .../Components/EntityKeyboard/BUILD | 2 +- .../Sources/EmojiPagerContentComponent.swift | 48 +- .../GiftAttributeListContextItem.swift | 1 + .../Sources/GlassBackgroundComponent.swift | 398 +++- .../Sources/LegacyGlassView.swift | 36 +- .../Sources/GlassBarButtonComponent.swift | 1 + .../Sources/GlassControlGroup.swift | 13 +- .../LegacyCamera/Sources/LegacyCamera.swift | 12 +- .../Sources/LegacyMessageInputPanel.swift | 6 +- .../Sources/ListActionItemComponent.swift | 2 + .../Components/MediaEditorScreen/BUILD | 3 +- .../Sources/AdjustmentsComponent.swift | 143 +- .../Sources/BlurComponent.swift | 42 +- .../Sources/CurvesComponent.swift | 18 +- .../Sources/MediaEditorScreen.swift | 320 +-- .../Sources/MediaToolsScreen.swift | 211 +- .../Sources/StickerPackListContextItem.swift | 1 + .../Sources/TintComponent.swift | 51 +- .../MediaNavigationAccessoryHeaderNode.swift | 1 + .../Sources/MessageInputPanelComponent.swift | 55 +- .../Sources/MoreHeaderButton.swift | 2 +- .../Sources/MultiplexedVideoNode.swift | 1 + .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 - .../Sources/Panes/PeerInfoGifPaneNode.swift | 1 + .../Sources/PeerInfoScreen.swift | 17 +- .../Sources/PeerInfoScreenAvatarSetup.swift | 4 +- .../Sources/PeerInfoScreenOpenMessage.swift | 3 + .../Sources/PeerInfoScreenOpenURL.swift | 4 +- .../Sources/StorySearchGridScreen.swift | 4 +- .../Sources/AddGiftsScreen.swift | 2 +- .../Sources/GiftsListView.swift | 2 +- .../Sources/PeerInfoGiftsPaneNode.swift | 3 +- .../Sources/PeerInfoStoryPaneNode.swift | 59 +- .../Sources/ProfileLevelInfoScreen.swift | 4 +- .../Components/SaveProgressScreen/BUILD | 1 + .../Components/SendInviteLinkScreen/BUILD | 1 - .../Sources/SendInviteLinkScreen.swift | 47 +- .../Sources/BusinessLocationSetupScreen.swift | 8 +- .../Sources/ChatbotSetupScreen.swift | 17 +- .../Sources/LanguageSelectionScreenNode.swift | 2 +- .../Sources/UserApperanceScreen.swift | 5 +- .../Sources/ItemListReactionItem.swift | 2 +- .../Sources/ReactionChatPreviewItem.swift | 2 +- .../Settings/ThemeAccentColorScreen/BUILD | 1 + .../Sources/ThemeAccentColorController.swift | 69 +- .../ThemeAccentColorControllerNode.swift | 19 +- .../ThemeColorSegmentedTitleView.swift | 67 - .../Sources/ThemeSettingsThemeItem.swift | 2 +- .../Settings/WallpaperGalleryScreen/BUILD | 3 + .../Sources/WallpaperColorPanelNode.swift | 332 ++-- .../Sources/WallpaperColorPickerNode.swift | 55 +- .../Sources/WallpaperCropNode.swift | 1 + .../Sources/WallpaperGalleryController.swift | 629 +++--- .../Sources/WallpaperGalleryItem.swift | 215 +- .../Sources/WallpaperGalleryToolbarNode.swift | 402 ++-- .../Sources/WallpaperOptionButtonNode.swift | 166 +- .../Sources/WallpaperPatternPanelNode.swift | 1 + .../ThemeColorsGridControllerItem.swift | 4 + .../ThemeColorsGridControllerNode.swift | 122 +- .../Sources/ThemeGridController.swift | 4 +- .../Sources/ThemeGridControllerItem.swift | 6 +- .../Sources/ThemeGridControllerNode.swift | 29 +- .../Sources/ThemeGridSearchColorsItem.swift | 257 --- .../Sources/ThemeGridSearchContentNode.swift | 833 -------- .../Sources/ThemeGridSearchItem.swift | 154 -- .../WallpaperSearchRecentQueries.swift | 67 - .../Sources/ShareWithPeersScreen.swift | 4 +- .../Sources/SliderComponent.swift | 197 +- .../Stories/StoryContainerScreen/BUILD | 1 - ...StoryItemSetContainerViewSendMessage.swift | 158 +- .../TelegramUIDeclareEncodables/BUILD | 1 - .../Sources/TelegramUIDeclareEncodables.swift | 1 - .../Sources/EditableTokenListNode.swift | 2 + ...anslationLanguagesContextMenuContent.swift | 1 + .../OptionGlobeAmerica.imageset/Contents.json | 12 + .../OptionGlobeAmerica.imageset/loc8.pdf | Bin 0 -> 9535 bytes .../OptionGlobeAsia.imageset/Contents.json | 12 + .../OptionGlobeAsia.imageset/loc9.pdf | Bin 0 -> 11223 bytes .../OptionGlobeEurope.imageset/Contents.json | 12 + .../OptionGlobeEurope.imageset/loc7.pdf | Bin 0 -> 10449 bytes .../OptionHybrid.imageset/Contents.json | 12 + .../Location/OptionHybrid.imageset/loc11.pdf | Bin 0 -> 10044 bytes .../OptionLocate.imageset/Contents.json | 2 +- .../Location/OptionLocate.imageset/loc1.pdf | Bin 0 -> 7747 bytes .../OptionLocating.imageset/Contents.json | 12 + .../loc2.pdf} | Bin 6388 -> 7490 bytes .../Location/OptionMap.imageset/Contents.json | 2 +- .../Location/OptionMap.imageset/loc5.pdf | Bin 0 -> 8152 bytes .../OptionTracking.imageset/Contents.json | 12 + .../Location/OptionTracking.imageset/loc4.pdf | Bin 0 -> 7724 bytes .../Contents.json | 2 +- .../Adjustments.imageset/tools_30 (4).pdf | Bin 0 -> 4094 bytes .../Media Editor/Crop.imageset/Contents.json | 12 + .../Crop.imageset/crop_30 (4).pdf | Bin 0 -> 4407 bytes .../Pencil.imageset/Contents.json | 2 +- .../Pencil.imageset/pencil_30 (4).pdf | Bin 0 -> 4181 bytes .../Pencil.imageset/pencil_30.pdf | 93 - .../Media Editor/Redo.imageset/Contents.json | 21 - .../Media Editor/Redo.imageset/undo.png | Bin 2175 -> 0 bytes .../Media Editor/Tools.imageset/tools_30.pdf | 101 - .../Media Editor/Undo.imageset/Contents.json | 13 +- .../Media Editor/Undo.imageset/undo.png | Bin 409 -> 0 bytes .../Undo.imageset/undo_30 (4).pdf} | Bin 6001 -> 6235 bytes .../Open In/Safari.imageset/200x200bb-100.png | Bin 0 -> 38486 bytes .../Open In/Safari.imageset/Contents.json | 9 +- .../Open In/Safari.imageset/Safari@2x.png | Bin 4143 -> 0 bytes .../Open In/Safari.imageset/Safari@3x.png | Bin 5449 -> 0 bytes .../Sources/Chat/ChatControllerEditGif.swift | 4 + ...ChatControllerOpenMessageContextMenu.swift | 8 +- .../ChatControllerOpenPollContextMenu.swift | 19 +- .../Chat/ChatControllerOpenWebApp.swift | 8 +- .../Chat/ChatMessageActionOptions.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 94 +- ...rollerDisplayGuestChatMessageTooltip.swift | 22 +- .../ChatControllerOpenAttachmentMenu.swift | 161 +- .../ChatInterfaceStateContextMenus.swift | 42 +- .../ChatInviteRequestsTitlePanelNode.swift | 21 +- .../ChatPinnedMessageTitlePanelNode.swift | 6 +- .../ContactSelectionControllerNode.swift | 2 +- .../Sources/CreateChannelController.swift | 3 +- .../Sources/CreateGroupController.swift | 55 +- .../Sources/InlineReactionSearchPanel.swift | 1 + .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 + submodules/TelegramUI/Sources/OpenUrl.swift | 11 +- .../OverlayAudioPlayerControllerNode.swift | 8 +- .../OverlayAudioPlayerControlsNode.swift | 6 + .../Sources/SharedAccountContext.swift | 12 +- .../Sources/SharedWakeupManager.swift | 6 +- .../Sources/PostboxKeys.swift | 4 - .../UrlHandling/Sources/UrlHandling.swift | 28 +- submodules/Weather/BUILD | 22 + .../Sources/Weather.swift | 127 +- submodules/WebSearchUI/BUILD | 39 - .../Sources/LegacyWebSearchEditor.swift | 97 - .../Sources/LegacyWebSearchGallery.swift | 474 ----- .../Sources/WebSearchBadgeNode.swift | 96 - .../Sources/WebSearchController.swift | 608 ------ .../Sources/WebSearchControllerNode.swift | 829 -------- .../Sources/WebSearchGalleryController.swift | 354 ---- .../WebSearchGalleryFooterContentNode.swift | 76 - .../Sources/WebSearchInterfaceState.swift | 43 - .../WebSearchUI/Sources/WebSearchItem.swift | 316 --- .../WebSearchNavigationContentNode.swift | 73 - .../Sources/WebSearchRecentQueries.swift | 68 - .../Sources/WebSearchRecentQueryItem.swift | 233 --- .../Sources/WebSearchVideoGalleryItem.swift | 545 ------ .../WebUI/Sources/WebAppController.swift | 43 +- .../Sources/WebAppSetEmojiStatusScreen.swift | 2 - submodules/WebUI/Sources/WebAppWebView.swift | 2 +- 351 files changed, 11565 insertions(+), 12031 deletions(-) create mode 100644 submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift delete mode 100644 submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift delete mode 100644 submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift create mode 100644 submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift create mode 100644 submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h create mode 100644 submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift delete mode 100644 submodules/LocationUI/Sources/LocationOptionsNode.swift create mode 100644 submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift rename submodules/OpenInExternalAppUI/Sources/{OpenInActionSheetController.swift => OpenInOptionsScreen.swift} (92%) delete mode 100644 submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift delete mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift delete mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift delete mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift delete mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/loc11.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json rename submodules/TelegramUI/Images.xcassets/Location/{OptionMap.imageset/headermap_30.pdf => OptionLocating.imageset/loc2.pdf} (59%) create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf rename submodules/TelegramUI/Images.xcassets/Media Editor/{Tools.imageset => Adjustments.imageset}/Contents.json (83%) create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/crop_30 (4).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json delete mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png rename submodules/TelegramUI/Images.xcassets/{Location/OptionLocate.imageset/headerlocation_30.pdf => Media Editor/Undo.imageset/undo_30 (4).pdf} (66%) create mode 100644 submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@2x.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png create mode 100644 submodules/Weather/BUILD rename submodules/{TelegramUI/Components/MediaEditorScreen => Weather}/Sources/Weather.swift (76%) delete mode 100644 submodules/WebSearchUI/BUILD delete mode 100644 submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift delete mode 100644 submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchController.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchControllerNode.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchGalleryController.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchItem.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift delete mode 100644 submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift diff --git a/Telegram/BUILD b/Telegram/BUILD index 22655f3a9b..b73ce7398c 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1570,6 +1570,8 @@ plist_fragment( here-location yandexmaps yandexnavi + yandextaxi + yangoride comgooglemaps youtube twitter @@ -1598,6 +1600,7 @@ plist_fragment( dolphin instagram-stories yangomaps + vivaldi LSRequiresIPhoneOS diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8bad338a88..a626678a02 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16339,3 +16339,6 @@ Error: %8$@"; "RecentSessions.ConnectedBot.TerminateCheckbox" = "Also terminate %@"; "Chat.GuestChatMessageTooltip" = "This bot can't read the chat – only the messages where it was mentioned."; + +"MediaPicker.SetNewGroupPhoto" = "Set new group photo"; +"MediaPicker.SetNewChannelPhoto" = "Set new channel photo"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4c8e9e4ac7..bd5b3a8537 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -185,6 +185,22 @@ public enum WallpaperUrlParameter { case gradient([UInt32], Int32?) } +public enum PeerType: Equatable { + case user(isBot: Bool) + case group + case channel + + public static func getType(for peer: EnginePeer) -> PeerType { + if case let .user(user) = peer { + return .user(isBot: user.botInfo != nil) + } else if case let .channel(channel) = peer, case .broadcast = channel.info { + return .channel + } else { + return .group + } + } +} + public struct ResolvedBotChoosePeerTypes: OptionSet { public var rawValue: UInt32 @@ -1484,7 +1500,7 @@ public protocol SharedAccountContext: AnyObject { func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerEditorScreen(context: AccountContext, source: Any?, mode: StickerEditorMode, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) + func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 39418cacac..09231d3c95 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -264,6 +264,23 @@ public extension ChatMessageItemAssociatedData { return false } } + + func isPollVotingRestricted(poll: TelegramMediaPoll, accountTestingEnvironment: Bool, currentTimestamp: Int32) -> Bool { + if !poll.countries.isEmpty, let accountCountry = self.accountCountry, !poll.countries.contains(accountCountry) { + return true + } + + if poll.restrictToSubscribers { + let period: Int32 = accountTestingEnvironment ? 5 * 60 : 24 * 60 * 60 + if !self.isParticipant { + return true + } else if let invitedOn = self.invitedOn, invitedOn + period > currentTimestamp { + return true + } + } + + return false + } } public enum ChatControllerInteractionLongTapAction { diff --git a/submodules/AdUI/Sources/AdInfoScreen.swift b/submodules/AdUI/Sources/AdInfoScreen.swift index 7cecf8224b..df5a2684fd 100644 --- a/submodules/AdUI/Sources/AdInfoScreen.swift +++ b/submodules/AdUI/Sources/AdInfoScreen.swift @@ -78,7 +78,7 @@ public final class AdInfoScreen: ViewController { self.scrollNode = ASScrollNode() self.scrollNode.view.showsVerticalScrollIndicator = true self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.scrollsToTop = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true if #available(iOS 11.0, *) { diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm index ba05b9d895..e0d85c8863 100644 --- a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -341,6 +341,7 @@ textView.opaque = NO; } textView.textContainerInset = self.textContainerInset; + textView.scrollsToTop = false; // Configure textView with UITextInputTraits { @@ -366,6 +367,7 @@ _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; _placeholderTextKitComponents.textView.userInteractionEnabled = NO; _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; + _placeholderTextKitComponents.textView.scrollsToTop = false; configureTextView(_placeholderTextKitComponents.textView); // Create and configure our text view. diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 9f3a8d8552..f60228bde9 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1563,12 +1563,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if let data = view.cachedData as? CachedUserData { return data.sendPaidMessageStars } else if let channel = peerViewMainPeer(view) as? TelegramChannel { - if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { - return nil - } else if let cachedData = view.cachedData as? CachedChannelData, let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero { - return nil + if channel.isMonoForum { + if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + return nil + } else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars, value == .zero { + return nil + } else { + return channel.sendPaidMessageStars + } } else { - return channel.sendPaidMessageStars + if channel.flags.contains(.isCreator) || channel.adminRights != nil { + return nil + } else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars { + return value == .zero ? nil : value + } else { + return channel.sendPaidMessageStars + } } } else { return nil @@ -1600,6 +1610,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.containerNode.layer.cornerCurve = .continuous } + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false diff --git a/submodules/BotPaymentsUI/BUILD b/submodules/BotPaymentsUI/BUILD index 45cfed992b..0b35e43a4f 100644 --- a/submodules/BotPaymentsUI/BUILD +++ b/submodules/BotPaymentsUI/BUILD @@ -11,10 +11,21 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LocalAuth:LocalAuth", "//submodules/AccountContext:AccountContext", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", "//submodules/ItemListUI:ItemListUI", "//submodules/PasswordSetupUI:PasswordSetupUI", "//submodules/PhotoResources:PhotoResources", diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index e970e67688..c63254c00e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -117,6 +117,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { } else { applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) } + applePayButton.cornerRadius = BotCheckoutActionButton.height * 0.5 applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside) self.view.addSubview(applePayButton) self.applePayButton = applePayButton diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 035fa49f99..dab1b83bd0 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -141,9 +141,10 @@ public final class BotCheckoutController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self._hasGlassStyle = true var title = self.presentationData.strings.Checkout_Title if invoice.flags.contains(.isTest) { @@ -151,7 +152,7 @@ public final class BotCheckoutController: ViewController { } self.title = title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index e81bf2e2a4..ee0a215277 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1077,13 +1077,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz if methods.isEmpty { openNewCard(nil, nil) } else { - strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in + strongSelf.controller?.push(BotCheckoutPaymentMethodScreen(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in applyPaymentMethod(method) }, newCard: { openNewCard(nil, nil) }, otherMethod: { url, title in openNewCard(url, title) - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + })) } } } @@ -1091,14 +1091,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openShippingMethodImpl = { [weak self] in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty { strongSelf.controller?.view.endEditing(true) - strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in + strongSelf.controller?.push(BotCheckoutShippingOptionScreen(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentShippingOptionId = id strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + })) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 84ff228769..0172056d75 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -206,7 +206,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { } let contentSize = CGSize(width: params.width, height: contentHeight) - let insets = itemListNeighborsPlainInsets(neighbors) + let insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift index bee72bda48..9a178d626c 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift @@ -60,14 +60,16 @@ final class BotCheckoutInfoController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) + + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.title = self.presentationData.strings.CheckoutInfo_Title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = self.doneItem self.doneItem?.isEnabled = false } @@ -84,13 +86,13 @@ final class BotCheckoutInfoController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) } } - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.push(controller) } }, updateStatus: { [weak self] status in if let strongSelf = self { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift index 353b1adfbf..cbc2ae8aff 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift @@ -455,7 +455,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, ASScrollVi let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var sideInset: CGFloat = 0.0 - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sideInset = inset } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index 74af758711..b1bc797121 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -55,14 +55,16 @@ final class BotCheckoutNativeCardEntryController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) + + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.title = self.presentationData.strings.Checkout_NewCard_Title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = self.doneItem self.doneItem?.isEnabled = false } @@ -78,13 +80,13 @@ final class BotCheckoutNativeCardEntryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) } } - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.push(controller) } }, updateStatus: { [weak self] status in if let strongSelf = self { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index 7a0c7afc0c..32025f0f50 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -468,7 +468,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var sideInset: CGFloat = 0.0 - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sideInset = inset } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift index 7e08b985b8..644e8ce4a6 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift @@ -1,374 +1,96 @@ import Foundation import UIKit -import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: TextFieldNode - private let placeholderNode: ASTextNode - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.textField.text ?? "" - } - set { - self.textInputNode.textField.text = newValue - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = TextFieldNode() - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] - self.textInputNode.textField.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.textField.returnKeyType = .done - self.textInputNode.textField.isSecureTextEntry = true - self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor - self.textInputNode.textField.textColor = theme.actionSheet.inputTextColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.textField.delegate = self - self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] - self.textInputNode.textField.textColor = self.theme.actionSheet.inputTextColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - func shake() { - self.layer.addShakeAnimation() +func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let inputState = AlertInputFieldComponent.ExternalState() + let doneInProgressPromise = ValuePromise(false) + let doneIsEnabled: Signal = combineLatest(inputState.valueSignal, doneInProgressPromise.get()) + |> map { value, isInProgress in + return !value.isEmpty && !isInProgress } - @objc func textDidChange() { - self.updateTextNodeText(animated: true) - self.textChanged?(self.textInputNode.textField.text ?? "") - self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.textField.text = nil - self.deactivateInput() - } -} + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Checkout_PasswordEntry_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Checkout_PasswordEntry_Text(cartTitle).string)) + ) + )) -private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { - private let context: AccountContext - private let period: Int32 - private let requiresBiometrics: Bool - private let completion: (TemporaryTwoStepPasswordToken) -> Void - - private let titleNode: ASTextNode - private let textNode: ASTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let cancelActionNode: TextAlertContentActionNode - private let doneActionNode: TextAlertContentActionNode - - let inputFieldNode: BotCheckoutPassworInputFieldNode - - private var validLayout: CGSize? - private var isVerifying = false - private let disposable = MetaDisposable() - - private let hapticFeedback = HapticFeedback() - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) { - self.context = context - self.period = period - self.requiresBiometrics = requiresBiometrics - self.completion = completion - - let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular) - - let titleNode = ASTextNode() - titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - titleNode.displaysAsynchronously = false - titleNode.isUserInteractionEnabled = false - titleNode.maximumNumberOfLines = 1 - titleNode.truncationMode = .byTruncatingTail - self.titleNode = titleNode - - self.textNode = ASTextNode() - self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).string, font: Font.regular(13.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - self.textNode.displaysAsynchronously = false - self.textNode.isUserInteractionEnabled = false - - self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "") - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor - - self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: { - cancel() - })) - - var doneImpl: (() -> Void)? - self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: { - doneImpl?() - })) - - self.actionNodes = [self.cancelActionNode, self.doneActionNode] - - var actionVerticalSeparators: [ASDisplayNode] = [] - if self.actionNodes.count > 1 { - for _ in 0 ..< self.actionNodes.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.addSubnode(self.inputFieldNode) - - self.inputFieldNode.textChanged = { [weak self] _ in - if let strongSelf = self { - strongSelf.updateState() - } - } - - self.updateState() - - doneImpl = { [weak self] in - self?.verify() - } + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: passwordTip ?? "", + isSecureTextEntry: true, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var isVerifying = false + let disposable = MetaDisposable() + var dismissImpl: (() -> Void)? + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Checkout_PasswordEntry_Pay, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: (initial: presentationData, signal: context.sharedContext.presentationData) + ) + alertController.dismissed = { _ in + disposable.dispose() } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let previousLayout = self.validLayout - self.validLayout = size - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - - let actionsHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionsHeight)) - minActionsWidth += actionTitleSize.width + actionTitleInsets - } - - let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth) - - let spacing: CGFloat = 6.0 - let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) - transition.updateFrame(node: titleNode, frame: titleFrame) - - let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0) - - let inputFieldWidth = resultSize.width - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight)) - - 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] - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - - let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) - - actionOffset += currentActionWidth - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if previousLayout == nil { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - @objc func textFieldChanged(_ textField: UITextField) { - self.updateState() - } - - private func updateState() { - var enabled = true - if self.isVerifying || self.inputFieldNode.text.isEmpty { - enabled = false - } - self.doneActionNode.actionEnabled = enabled - } - - private func verify() { - let text = self.inputFieldNode.text - guard !text.isEmpty else { + + applyImpl = { + let password = inputState.value + guard !isVerifying, !password.isEmpty else { return } - - self.isVerifying = true - self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in - if let strongSelf = self { - strongSelf.completion(token) - } - }, error: { [weak self] _ in - if let strongSelf = self { - strongSelf.inputFieldNode.shake() - strongSelf.hapticFeedback.error() - strongSelf.isVerifying = false - strongSelf.updateState() - } - })) - self.updateState() - } -} -func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { - var dismissImpl: (() -> Void)? - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { - dismissImpl?() - }, completion: { token in - completion(token) - dismissImpl?() - })) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() + isVerifying = true + doneInProgressPromise.set(true) + + disposable.set((context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: period, requiresBiometrics: requiresBiometrics) + |> deliverOnMainQueue).start(next: { token in + completion(token) + dismissImpl?() + }, error: { _ in + inputState.animateError() + isVerifying = false + doneInProgressPromise.set(false) + })) } - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift new file mode 100644 index 0000000000..1e85b5f263 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift @@ -0,0 +1,536 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import AccountContext +import ViewControllerComponent +import ResizableSheetComponent +import TelegramPresentationData +import PresentationDataUtils +import MultilineTextComponent +import ButtonComponent +import ListSectionComponent +import ListActionItemComponent +import GlassBarButtonComponent +import BundleIconComponent + +struct BotCheckoutPaymentWebToken: Equatable { + let title: String + let data: String + var saveOnServer: Bool +} + +enum BotCheckoutPaymentMethod: Equatable { + case savedCredentials(BotPaymentSavedCredentials) + case webToken(BotCheckoutPaymentWebToken) + case applePay + case other(BotPaymentMethod) + + var title: String { + switch self { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, title): + return title + } + case let .webToken(token): + return token.title + case .applePay: + return "Apple Pay" + case let .other(method): + return method.title + } + } +} + +private func splitSavedCardTitle(_ title: String) -> (String, String?) { + guard let separatorIndex = title.lastIndex(of: "*") else { + return (title, nil) + } + + let name = String(title[.. Void + let addCard: () -> Void + + init( + methods: [BotCheckoutPaymentMethod], + selectedMethod: BotCheckoutPaymentMethod?, + selectMethod: @escaping (BotCheckoutPaymentMethod) -> Void, + addCard: @escaping () -> Void + ) { + self.methods = methods + self.selectedMethod = selectedMethod + self.selectMethod = selectMethod + self.addCard = addCard + } + + static func ==(lhs: BotCheckoutPaymentMethodContentComponent, rhs: BotCheckoutPaymentMethodContentComponent) -> Bool { + if lhs.methods != rhs.methods { + return false + } + if lhs.selectedMethod != rhs.selectedMethod { + return false + } + return true + } + + final class View: UIView { + private let section = ComponentView() + + private var component: BotCheckoutPaymentMethodContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutPaymentMethodContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let theme = environment.theme.withModalBlocksBackground() + let itemFontSize: CGFloat = 17.0 + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 76.0 + 9.0 + + var items: [AnyComponentWithIdentity] = [] + for i in 0 ..< component.methods.count { + let method = component.methods[i] + let isSelected = method == component.selectedMethod + + var title = method.title + var icon: ListActionItemComponent.Icon? + var accessory: ListActionItemComponent.Accessory? + + switch method { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, cardTitle): + let (cardName, cardSuffix) = splitSavedCardTitle(cardTitle) + title = cardName + if let cardSuffix { + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: cardSuffix, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )) + } + } + case .applePay: + title = "Apple Pay" + icon = ListActionItemComponent.Icon( + component: AnyComponentWithIdentity( + id: AnyHashable("apple-pay"), + component: AnyComponent(BundleIconComponent( + name: "Bot Payments/ApplePayLogo", + tintColor: nil + )) + ) + ) + case let .webToken(token): + let (cardName, cardSuffix) = splitSavedCardTitle(token.title) + title = cardName + if let cardSuffix { + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: cardSuffix, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )) + } + case let .other(method): + title = method.title + } + + items.append(AnyComponentWithIdentity(id: AnyHashable("method-\(i)"), component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(itemFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: isSelected, + toggle: { + component.selectMethod(method) + } + )), + icon: icon, + accessory: accessory, + action: { _ in + component.selectMethod(method) + } + )))) + } + + items.append(AnyComponentWithIdentity(id: AnyHashable("add-card"), component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Checkout_PaymentMethod_New, + font: Font.regular(itemFontSize), + textColor: theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + )), + contentInsets: UIEdgeInsets(top: 12.0, left: 45.0, bottom: 12.0, right: 0.0), + leftIcon: nil, + icon: nil, + accessory: nil, + action: { _ in + component.addCard() + } + )))) + + self.section.parentState = state + let sectionSize = self.section.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: items + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize) + if let sectionView = self.section.view { + if sectionView.superview == nil { + self.addSubview(sectionView) + } + transition.setFrame(view: sectionView, frame: sectionFrame) + } + contentHeight += sectionSize.height + contentHeight += 112.0 + + 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: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class BotCheckoutPaymentMethodScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let currentMethod: BotCheckoutPaymentMethod? + let methods: [BotCheckoutPaymentMethod] + let applyValue: (BotCheckoutPaymentMethod) -> Void + let newCard: () -> Void + let otherMethod: (String, String) -> Void + + init( + context: AccountContext, + currentMethod: BotCheckoutPaymentMethod?, + methods: [BotCheckoutPaymentMethod], + applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, + newCard: @escaping () -> Void, + otherMethod: @escaping (String, String) -> Void + ) { + self.context = context + self.currentMethod = currentMethod + self.methods = methods + self.applyValue = applyValue + self.newCard = newCard + self.otherMethod = otherMethod + } + + static func ==(lhs: BotCheckoutPaymentMethodScreenComponent, rhs: BotCheckoutPaymentMethodScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.currentMethod != rhs.currentMethod { + return false + } + if lhs.methods != rhs.methods { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: BotCheckoutPaymentMethodScreenComponent? + private weak var state: EmptyComponentState? + private var selectedMethod: BotCheckoutPaymentMethod? + private var isDismissing = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutPaymentMethodScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + if let currentMethod = component.currentMethod, component.methods.contains(currentMethod) { + self.selectedMethod = currentMethod + } else { + self.selectedMethod = nil + } + } + + self.component = component + self.state = state + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + + let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in + guard let self, !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? BotCheckoutPaymentMethodScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } + completion?() + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(BotCheckoutPaymentMethodContentComponent( + methods: component.methods, + selectedMethod: self.selectedMethod, + selectMethod: { [weak self] method in + guard let self else { + return + } + self.selectedMethod = method + self.state?.updated(transition: .spring(duration: 0.35)) + }, + addCard: { [weak self] in + guard let self, let component = self.component else { + return + } + dismiss(true, { + component.newCard() + }) + } + )), + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environmentValue.strings.Checkout_PaymentMethod, + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true, nil) + } + ) + ), + //TODO:localize + bottomItem: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("proceed"), + component: AnyComponent(ButtonTextContentComponent( + text: "Proceed", + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: self.selectedMethod != nil, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedMethod = self.selectedMethod else { + return + } + dismiss(true, { + switch selectedMethod { + case let .other(method): + component.otherMethod(method.url, method.title) + default: + component.applyValue(selectedMethod) + } + }) + } + )), + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + animateOut: self.animateOut + )), + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated, nil) + } + ) + }, + forceUpdate: true, + containerSize: availableSize + ) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BotCheckoutPaymentMethodScreen: ViewControllerComponentContainer { + private var isDismissed = false + private var dismissCompletion: (() -> Void)? + + init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) { + super.init( + context: context, + component: BotCheckoutPaymentMethodScreenComponent( + context: context, + currentMethod: currentMethod, + methods: methods, + applyValue: applyValue, + newCard: newCard, + otherMethod: otherMethod + ), + navigationBarAppearance: .none + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + self.dismissCompletion = completion + + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } else { + self.completePendingDismiss() + self.dismiss(animated: false) + } + } + } +} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift deleted file mode 100644 index a728d78155..0000000000 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift +++ /dev/null @@ -1,262 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import AccountContext -import AppBundle - -struct BotCheckoutPaymentWebToken: Equatable { - let title: String - let data: String - var saveOnServer: Bool -} - -enum BotCheckoutPaymentMethod: Equatable { - case savedCredentials(BotPaymentSavedCredentials) - case webToken(BotCheckoutPaymentWebToken) - case applePay - case other(BotPaymentMethod) - - var title: String { - switch self { - case let .savedCredentials(credentials): - switch credentials { - case let .card(_, title): - return title - } - case let .webToken(token): - return token.title - case .applePay: - return "Apple Pay" - case let .other(method): - return method.title - } - } -} - -final class BotCheckoutPaymentMethodSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }).strict() - - var items: [ActionSheetItem] = [] - - items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod)) - - for method in methods { - let title: String - let icon: UIImage? - switch method { - case let .savedCredentials(credentials): - switch credentials { - case let .card(_, cardTitle): - title = cardTitle - icon = nil - } - case let .webToken(token): - title = token.title - icon = nil - case .applePay: - title = "Apple Pay" - icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed() - case let .other(method): - title = method.title - icon = nil - } - let value: Bool? - if let currentMethod = currentMethod { - value = method == currentMethod - } else { - value = nil - } - items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in - if case let .other(method) = method { - otherMethod(method.url, method.title) - } else { - applyValue(method) - } - self?.dismissAnimated() - })) - } - - items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in - self?.dismissAnimated() - newCard() - })) - - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -public class BotCheckoutPaymentMethodItem: ActionSheetItem { - public let title: String - public let icon: UIImage? - public let value: Bool? - public let action: (Bool) -> Void - - public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) { - self.title = title - self.icon = icon - self.value = value - self.action = action - } - - public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - let node = BotCheckoutPaymentMethodItemNode(theme: theme) - node.setItem(self) - return node - } - - public func updateNode(_ node: ActionSheetItemNode) { - guard let node = node as? BotCheckoutPaymentMethodItemNode else { - assertionFailure() - return - } - - node.setItem(self) - node.requestLayoutUpdate() - } -} - -public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { - private let defaultFont: UIFont - - private let theme: ActionSheetControllerTheme - - private var item: BotCheckoutPaymentMethodItem? - - private let button: HighlightTrackingButton - private let titleNode: ASTextNode - private let iconNode: ASImageNode - private let checkNode: ASImageNode - - public override init(theme: ActionSheetControllerTheme) { - self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.button = HighlightTrackingButton() - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - self.checkNode = ASImageNode() - self.checkNode.isUserInteractionEnabled = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.displaysAsynchronously = false - self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.controlAccentColor.cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() - }) - - super.init(theme: theme) - - self.view.addSubview(self.button) - self.addSubnode(self.titleNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.checkNode) - - self.button.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.backgroundColor = theme.itemHighlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor - }) - } - } - } - - self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) - } - - func setItem(_ item: BotCheckoutPaymentMethodItem) { - self.item = item - - self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) - self.iconNode.image = item.icon - if let value = item.value { - self.checkNode.isHidden = !value - } else { - self.checkNode.isHidden = true - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - var checkInset: CGFloat = 15.0 - if let _ = self.item?.value { - checkInset = 44.0 - } - - let iconSize: CGSize - if let image = self.iconNode.image { - iconSize = image.size - } else { - iconSize = CGSize() - } - let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) - self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) - - if let image = self.checkNode.image { - self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func buttonPressed() { - if let item = self.item { - let updatedValue: Bool - if let value = item.value { - updatedValue = !value - } else { - updatedValue = true - } - item.action(updatedValue) - } - } -} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift deleted file mode 100644 index 10b86ab94f..0000000000 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift +++ /dev/null @@ -1,224 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import AccountContext -import TelegramStringFormatting - -final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }).strict() - - var items: [ActionSheetItem] = [] - - items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod)) - - let dismissAction: () -> Void = { [weak self] in - self?.dismissAnimated() - } - - let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in - for i in 0 ..< options.count { - self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in - if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value { - return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action) - } - return item - }) - } - applyValue(id) - dismissAction() - } - - var itemIndex = 0 - for option in options { - let index = itemIndex - var totalPrice: Int64 = 0 - for price in option.prices { - totalPrice += price.amount - } - let value: Bool? - if let currentId = currentId { - value = option.id == currentId - } else { - value = nil - } - items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in - toggleCheck(option.id, index) - })) - itemIndex += 1 - } - - self.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in - self?.dismissAnimated() - }), - ]) - ]) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDisposable?.dispose() - } -} - -public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem { - public let title: String - public let label: String - public let value: Bool? - public let action: (Bool) -> Void - - public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) { - self.title = title - self.label = label - self.value = value - self.action = action - } - - public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - let node = BotCheckoutPaymentShippingOptionItemNode(theme: theme) - node.setItem(self) - return node - } - - public func updateNode(_ node: ActionSheetItemNode) { - guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else { - assertionFailure() - return - } - - node.setItem(self) - node.requestLayoutUpdate() - } -} - -public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { - private let defaultFont: UIFont - - private let theme: ActionSheetControllerTheme - - private var item: BotCheckoutPaymentShippingOptionItem? - - private let button: HighlightTrackingButton - private let titleNode: ASTextNode - private let labelNode: ASTextNode - private let checkNode: ASImageNode - - public override init(theme: ActionSheetControllerTheme) { - self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.button = HighlightTrackingButton() - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.labelNode = ASTextNode() - self.labelNode.maximumNumberOfLines = 1 - self.labelNode.isUserInteractionEnabled = false - self.labelNode.displaysAsynchronously = false - - self.checkNode = ASImageNode() - self.checkNode.isUserInteractionEnabled = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.displaysAsynchronously = false - self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.controlAccentColor.cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() - }) - - super.init(theme: theme) - - self.view.addSubview(self.button) - self.addSubnode(self.titleNode) - self.addSubnode(self.labelNode) - self.addSubnode(self.checkNode) - - self.button.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor - }) - } - } - } - - self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) - } - - func setItem(_ item: BotCheckoutPaymentShippingOptionItem) { - self.item = item - - self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) - self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor) - if let value = item.value { - self.checkNode.isHidden = !value - } else { - self.checkNode.isHidden = true - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - var checkInset: CGFloat = 15.0 - if let _ = self.item?.value { - checkInset = 44.0 - } - - let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height)) - let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) - self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) - - if let image = self.checkNode.image { - self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func buttonPressed() { - if let item = self.item { - let updatedValue: Bool - if let value = item.value { - updatedValue = !value - } else { - updatedValue = true - } - item.action(updatedValue) - } - } -} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift new file mode 100644 index 0000000000..aa848c41fc --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift @@ -0,0 +1,418 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramStringFormatting +import ViewControllerComponent +import ResizableSheetComponent +import TelegramPresentationData +import PresentationDataUtils +import MultilineTextComponent +import ButtonComponent +import ListSectionComponent +import ListActionItemComponent +import GlassBarButtonComponent +import BundleIconComponent + +private final class BotCheckoutShippingOptionContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let currency: String + let options: [BotPaymentShippingOption] + let selectedId: String? + let selectOption: (String) -> Void + + init( + currency: String, + options: [BotPaymentShippingOption], + selectedId: String?, + selectOption: @escaping (String) -> Void + ) { + self.currency = currency + self.options = options + self.selectedId = selectedId + self.selectOption = selectOption + } + + static func ==(lhs: BotCheckoutShippingOptionContentComponent, rhs: BotCheckoutShippingOptionContentComponent) -> Bool { + if lhs.currency != rhs.currency { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.selectedId != rhs.selectedId { + return false + } + return true + } + + final class View: UIView { + private let section = ComponentView() + + private var component: BotCheckoutShippingOptionContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutShippingOptionContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let theme = environment.theme.withModalBlocksBackground() + let itemFontSize: CGFloat = 17.0 + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 76.0 + 9.0 + + var items: [AnyComponentWithIdentity] = [] + for option in component.options { + let totalPrice = option.prices.reduce(Int64(0)) { current, price in + return current + price.amount + } + let priceText = formatCurrencyAmount(totalPrice, currency: component.currency) + let isSelected = option.id == component.selectedId + + items.append(AnyComponentWithIdentity(id: option.id, component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: option.title, + font: Font.regular(itemFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: isSelected, + toggle: { + component.selectOption(option.id) + } + )), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("price-\(option.id)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: priceText, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )), + action: { _ in + component.selectOption(option.id) + } + )))) + } + + self.section.parentState = state + let sectionSize = self.section.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: items + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize) + if let sectionView = self.section.view { + if sectionView.superview == nil { + self.addSubview(sectionView) + } + transition.setFrame(view: sectionView, frame: sectionFrame) + } + contentHeight += sectionSize.height + contentHeight += 112.0 + + 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: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class BotCheckoutShippingOptionScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let currency: String + let options: [BotPaymentShippingOption] + let currentId: String? + let applyValue: (String) -> Void + + init( + context: AccountContext, + currency: String, + options: [BotPaymentShippingOption], + currentId: String?, + applyValue: @escaping (String) -> Void + ) { + self.context = context + self.currency = currency + self.options = options + self.currentId = currentId + self.applyValue = applyValue + } + + static func ==(lhs: BotCheckoutShippingOptionScreenComponent, rhs: BotCheckoutShippingOptionScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.currency != rhs.currency { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.currentId != rhs.currentId { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: BotCheckoutShippingOptionScreenComponent? + private weak var state: EmptyComponentState? + private var selectedId: String? + private var isDismissing = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutShippingOptionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + if let currentId = component.currentId, component.options.contains(where: { $0.id == currentId }) { + self.selectedId = currentId + } else { + self.selectedId = nil + } + } + + self.component = component + self.state = state + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + + let dismiss: (Bool) -> Void = { [weak self] animated in + guard let self, !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? BotCheckoutShippingOptionScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(BotCheckoutShippingOptionContentComponent( + currency: component.currency, + options: component.options, + selectedId: self.selectedId, + selectOption: { [weak self] id in + guard let self else { + return + } + self.selectedId = id + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environmentValue.strings.Checkout_ShippingMethod, + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + //TODO:localize + bottomItem: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("proceed"), + component: AnyComponent(ButtonTextContentComponent( + text: "Proceed", + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: self.selectedId != nil, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedId = self.selectedId else { + return + } + component.applyValue(selectedId) + dismiss(true) + } + )), + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + animateOut: self.animateOut + )), + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + forceUpdate: true, + containerSize: availableSize + ) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BotCheckoutShippingOptionScreen: ViewControllerComponentContainer { + private var isDismissed = false + private var dismissCompletion: (() -> Void)? + + init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) { + super.init( + context: context, + component: BotCheckoutShippingOptionScreenComponent( + context: context, + currency: currency, + options: options, + currentId: currentId, + applyValue: applyValue + ), + navigationBarAppearance: .none + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + self.dismissCompletion = completion + + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } else { + self.completePendingDismiss() + self.dismiss(animated: false) + } + } + } +} diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 0f926508c3..6ca5cb0f41 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -65,7 +65,7 @@ public final class BrowserBookmarksScreen: ViewController { }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -83,8 +83,8 @@ public final class BrowserBookmarksScreen: ViewController { controller.dismiss() } }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { message, _ in if let openMessageImpl = openMessageImpl { @@ -134,7 +134,7 @@ public final class BrowserBookmarksScreen: ViewController { }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 996b15b381..b79600fc11 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -166,6 +166,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delegate = self + self.scrollNode.view.scrollsToTop = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never @@ -1532,12 +1533,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in if let self { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - self.present(actionSheet, nil) + self.push(actionSheet) } private func openMedia(_ media: InstantPageMedia) { diff --git a/submodules/BrowserUI/Sources/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift index c63a4469bf..bef6834e7c 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -34,7 +34,40 @@ public class Readability: NSObject, WKNavigationDelegate { if let (html, subresources) = extractHtmlString(from: archiveData) { self.subresources = subresources - self.webView.loadHTMLString(html, baseURL: url.baseURL) + self.sanitizeHtmlString(html) { [weak self] html in + guard let self else { + return + } + self.webView.loadHTMLString(html, baseURL: url.baseURL) + } + } + } + + private func sanitizeHtmlString(_ html: String, completion: @escaping (String) -> Void) { + guard let readerModeJS = loadFile(name: "ReaderMode", type: "js") else { + completion(htmlByRemovingScriptTags(html)) + return + } + + let domPurifyJS = extractDOMPurifyScript(from: readerModeJS) ?? readerModeJS + self.webView.evaluateJavaScript(domPurifyJS) { [weak self] _, error in + guard let self else { + return + } + + guard error == nil, let htmlLiteral = javascriptStringLiteral(html) else { + completion(htmlByRemovingScriptTags(html)) + return + } + + let sanitizeJS = """ + (function(html) { + return DOMPurify.sanitize(html, {WHOLE_DOCUMENT: true, ADD_TAGS: ["iframe"]}); + })(\(htmlLiteral)); + """ + self.webView.evaluateJavaScript(sanitizeJS) { result, _ in + completion((result as? String) ?? htmlByRemovingScriptTags(html)) + } } } @@ -49,6 +82,7 @@ public class Readability: NSObject, WKNavigationDelegate { return } guard let page = parseJson(result, url: self.url.absoluteString) else { + completion(nil, error) return } completion(page, nil) @@ -95,6 +129,32 @@ func loadFile(name: String, type: String) -> String? { return userScript } +private func extractDOMPurifyScript(from readerModeJS: String) -> String? { + guard let range = readerModeJS.range(of: "\n\n(function () {") else { + return nil + } + return String(readerModeJS[.. String? { + guard let data = try? JSONSerialization.data(withJSONObject: [input], options: []), + var arrayString = String(data: data, encoding: .utf8), + arrayString.count >= 2 else { + return nil + } + arrayString.removeFirst() + arrayString.removeLast() + return arrayString +} + +private func htmlByRemovingScriptTags(_ input: String) -> String { + guard let regex = try? NSRegularExpression(pattern: "]*>[\\s\\S]*?", options: [.caseInsensitive]) else { + return input + } + let range = NSRange(input.startIndex ..< input.endIndex, in: input) + return regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: "") +} + private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? { if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any], let mainResource = webArchiveDict["WebMainResource"] as? [String: Any], @@ -708,30 +768,38 @@ private func parseVideo(_ input: [String: Any], _ media: inout [EngineMedia.Id: ) } +private func firstElement(withTag tag: String, in input: [Any], skippingSubtreesWithTag skippedTag: String? = nil) -> [String: Any]? { + for item in input { + guard let item = item as? [String: Any] else { + continue + } + let itemTag = item["tag"] as? String + if itemTag == tag { + return item + } + if itemTag == skippedTag { + continue + } + if let content = item["content"] as? [Any], let result = firstElement(withTag: tag, in: content, skippingSubtreesWithTag: skippedTag) { + return result + } + } + return nil +} + private func parseFigure(_ input: [String: Any], _ media: inout [EngineMedia.Id: EngineRawMedia]) -> InstantPageBlock? { guard let content = input["content"] as? [Any] else { return nil } var block: InstantPageBlock? var caption: RichText? - for item in content { - if let item = item as? [String: Any], let tag = item["tag"] as? String { - if tag == "p", let content = item["content"] as? [Any] { - for item in content { - if let item = item as? [String: Any], let tag = item["tag"] as? String { - if tag == "iframe" { - block = parseVideo(item, &media) - } - } - } - } else if tag == "iframe" { - block = parseVideo(item, &media) - } else if tag == "img" { - block = parseImage(item, &media) - } else if tag == "figcaption" { - caption = trim(parseRichText(item, &media)) - } - } + if let iframe = firstElement(withTag: "iframe", in: content, skippingSubtreesWithTag: "figcaption") { + block = parseVideo(iframe, &media) + } else if let image = firstElement(withTag: "img", in: content, skippingSubtreesWithTag: "figcaption") { + block = parseImage(image, &media) + } + if let figcaption = firstElement(withTag: "figcaption", in: content) { + caption = trim(parseRichText(figcaption, &media)) } guard var block else { return nil diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index eb51c84e4b..28bf96d2a9 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -1868,7 +1868,7 @@ public final class CalendarMessageScreen: ViewController { self._hasGlassStyle = true self.navigationPresentation = .modal - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(dismissPressed)), animated: false) self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false) if self.enableMessageRangeDeletion { @@ -1894,7 +1894,7 @@ public final class CalendarMessageScreen: ViewController { self.node.toggleSelectionMode() if self.node.selectionState != nil { - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } else { self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } @@ -1904,7 +1904,7 @@ public final class CalendarMessageScreen: ViewController { self.node.selectDay(timestamp: timestamp) if self.node.selectionState != nil { - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } } diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 69e8defa35..97ec0f7cd9 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -213,7 +213,7 @@ public final class CallListController: TelegramBaseController { if let isEmpty = self.isEmpty, isEmpty { } else { if self.editingMode { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -222,7 +222,7 @@ public final class CallListController: TelegramBaseController { //self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) case .navigation: if self.editingMode { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -679,7 +679,7 @@ public final class CallListController: TelegramBaseController { switch self.mode { case .tab: - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true) self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true) self.navigationItem.rightBarButtonItem?.setCustomAction({ @@ -691,7 +691,7 @@ public final class CallListController: TelegramBaseController { pressedImpl?() }) - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true) } self.controllerNode.updateState { state in diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index b4b90095fa..ca0a001822 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -940,7 +940,7 @@ final class CallListControllerNode: ASDisplayNode { insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) let inset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { inset = 0.0 diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 04d25d6b9d..ed6f05ff25 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -129,6 +129,7 @@ swift_library( "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 3eb6a5ffe2..5ac75a9fd4 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -6244,7 +6244,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location { super.setToolbar(toolbar, transition: transition) } else { - self.chatListDisplayNode.toolbar = toolbar + self.chatListDisplayNode.toolbarData = toolbar self.requestLayout(transition: transition) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 0a983f6bd2..61cbefe701 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -28,6 +28,7 @@ import MediaPlaybackHeaderPanelComponent import LiveLocationHeaderPanelComponent import ChatListHeaderNoticeComponent import ChatListFilterTabContainerNode +import GlassControls public enum ChatListContainerNodeFilter: Equatable { case all @@ -1134,8 +1135,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { let navigationBarView = ComponentView() weak var controller: ChatListControllerImpl? - var toolbar: Toolbar? - private var toolbarNode: ToolbarNode? + private var toolbar: ComponentView? + var toolbarData: Toolbar? var toolbarActionSelected: ((ToolbarActionOption) -> Void)? private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch? @@ -1395,10 +1396,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.mainContainerNode.updatePresentationData(presentationData) self.inlineStackContainerNode?.updatePresentationData(presentationData) self.searchDisplayController?.updatePresentationData(presentationData) - - if let toolbarNode = self.toolbarNode { - toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme)) - } } private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { @@ -1842,53 +1839,101 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right - if let toolbar = self.toolbar { - var tabBarHeight: CGFloat - var options: ContainerViewLayoutInsetOptions = [] - if layout.metrics.widthClass == .regular { - options.insert(.input) + if let toolbarData = self.toolbarData { + var panelsBottomInset: CGFloat = layout.insets(options: []).bottom + if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 } - - var heightInset: CGFloat = 0.0 - if case .forum = self.location { - heightInset = 4.0 - } - - let bottomInset: CGFloat = layout.insets(options: options).bottom - if !layout.safeInsets.left.isZero { - tabBarHeight = 34.0 + bottomInset - insets.bottom += 34.0 + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - tabBarHeight = 49.0 - heightInset + bottomInset - insets.bottom += 49.0 - heightInset + panelsBottomInset = max(panelsBottomInset, 8.0) } - let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + let sideInset: CGFloat = 20.0 + let toolbarHeight = 44.0 + let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight)) - if let toolbarNode = self.toolbarNode { - transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition) + let toolbar: ComponentView + var toolbarTransition = ComponentTransition(transition) + if let current = self.toolbar { + toolbar = current } else { - let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in - self?.toolbarActionSelected?(.left) - }, right: { [weak self] in - self?.toolbarActionSelected?(.right) - }, middle: { [weak self] in - self?.toolbarActionSelected?(.middle) - }) - toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) - self.addSubnode(toolbarNode) - self.toolbarNode = toolbarNode - if transition.isAnimated { - toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + toolbar = ComponentView() + self.toolbar = toolbar + toolbarTransition = .immediate + } + + let _ = toolbar.update( + transition: toolbarTransition, + component: AnyComponent(GlassControlPanelComponent( + theme: self.presentationData.theme, + leftItem: toolbarData.leftAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "left_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.left) + } : nil + )], + background: .panel + ) + }, + centralItem: toolbarData.middleAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.middle) + } : nil + )], + background: .panel + ) + }, + rightItem: toolbarData.rightAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.right) + } : nil + )], + background: .panel + ) + }, + centerAlignmentIfPossible: true + )), + environment: {}, + containerSize: toolbarFrame.size + ) + + if let toolbarView = toolbar.view { + if toolbarView.superview == nil { + self.view.addSubview(toolbarView) + toolbarView.alpha = 0.0 } + toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame) + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0) + } + } else if let toolbar = self.toolbar { + self.toolbar = nil + if let toolbarView = toolbar.view { + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in + toolbarView?.removeFromSuperview() + }) } - } else if let toolbarNode = self.toolbarNode { - self.toolbarNode = nil - transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in - toolbarNode?.removeFromSupernode() - }) } var childrenLayout = layout diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index d3d58ff9de..54e099ffe6 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -2071,6 +2071,11 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese ) |> deliverOnMainQueue |> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + let (state, includePeers, excludePeers) = stateWithPeers let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index ab6adc27d0..f4817f217f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -613,6 +613,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch ) ) |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + let isPremium = peer?.isPremium ?? false let limits = allLimits.0 let premiumLimits = allLimits.1 diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 476f63b287..61195b1c04 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -425,9 +425,9 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 1.0), size: titleLayout.size)) - let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset + 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift index 4cce378d6a..a2aa4bccb7 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift @@ -215,14 +215,8 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi 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 verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 15.0 - case .legacy: - verticalInset = 11.0 - } - let titleSpacing: CGFloat = 3.0 + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 2.0 let height: CGFloat height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 8dda0ed77c..498ebd2303 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -6542,10 +6542,10 @@ private final class EmptyResultsButton: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: component.theme.list.itemCheckColors.fillColor, foreground: component.theme.list.itemCheckColors.foregroundColor, - pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: buttonContent, isEnabled: isEnabled, @@ -6557,7 +6557,7 @@ private final class EmptyResultsButton: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 50.0) + containerSize: CGSize(width: availableSize.width, height: 52.0) ) if let buttonView = self.button.view { if buttonView.superview == nil { diff --git a/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift index 411cd295b9..4abb01fb48 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift @@ -185,6 +185,7 @@ class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate { self.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index b5cb366326..9b87594089 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -640,9 +640,9 @@ private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_un private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", scale: 1.0, offset: 0.0, replaceColors: nil, flip: false) private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", scale: 1.0, offset: 0.0, replaceColors: [0x2194fa], flip: false) private let archiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_archive", scale: 1.0, offset: 2.0, replaceColors: [0xa9a9ad], flip: false) -private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.642, offset: -9.0, replaceColors: [0xa9a9ad], flip: false) -private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: 2.0, replaceColors: [0xbdbdc2], flip: false) -private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -20.0, replaceColors: [0xbdbdc2], flip: true) +private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.52, offset: -6.0, replaceColors: [0xa9a9ad], flip: false) +private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.1, offset: 2.0, replaceColors: [0xbdbdc2], flip: false) +private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -15.0, replaceColors: [0xbdbdc2], flip: true) private let startIcon = ItemListRevealOptionIcon.animation(animation: "anim_play", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false) private let closeIcon = ItemListRevealOptionIcon.animation(animation: "anim_pause", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false) @@ -1393,8 +1393,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { private var isHighlighted: Bool = false private var isRevealHighlighted: Bool = false - private var isSeparatorHiddenByLowerReveal: Bool = false - private weak var revealHighlightedUpperNeighbor: ChatListItemNode? + private var keepRevealHighlightUntilClosed: Bool = false private var nextHasActiveRevealControls: Bool = false private var skipFadeout: Bool = false private var customAnimationInProgress: Bool = false @@ -1744,7 +1743,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } deinit { - self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: .immediate) self.cachedDataDisposable.dispose() } @@ -2147,42 +2145,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) { - let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls || self.isSeparatorHiddenByLowerReveal) ? 0.0 : 1.0 + let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls) ? 0.0 : 1.0 if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress { transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha) } else { transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha) } } - - private func updateSeparatorHiddenByLowerReveal(_ value: Bool, transition: ContainedViewLayoutTransition) { - self.isSeparatorHiddenByLowerReveal = value - self.updateSeparatorAlpha(transition: transition) - } - - private func updateUpperNeighborSeparatorForReveal(_ value: Bool, transition: ContainedViewLayoutTransition) { - if value { - var upperNeighbor: ChatListItemNode? - if let supernode = self.supernode, let subnodes = supernode.subnodes { - for case let node as ChatListItemNode in subnodes { - if node !== self && abs(node.frame.maxY - self.frame.minY) <= 2.0 { - if upperNeighbor == nil || node.frame.maxY > upperNeighbor!.frame.maxY { - upperNeighbor = node - } - } - } - } - - if self.revealHighlightedUpperNeighbor !== upperNeighbor { - self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: transition) - self.revealHighlightedUpperNeighbor = upperNeighbor - } - upperNeighbor?.updateSeparatorHiddenByLowerReveal(true, transition: transition) - } else if let upperNeighbor = self.revealHighlightedUpperNeighbor { - upperNeighbor.updateSeparatorHiddenByLowerReveal(false, transition: transition) - self.revealHighlightedUpperNeighbor = nil - } - } override public func tapped() { guard let item = self.item, item.editing else { @@ -3559,7 +3528,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent) var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)? - if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) } @@ -4413,16 +4382,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent) + let rightAccessoryVerticalOffset: CGFloat = -2.0 var nextBadgeX: CGFloat = contentRect.maxX if let _ = currentBadgeBackgroundImage { - let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height) transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame) nextBadgeX -= badgeLayout.width + 6.0 } if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil { - let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame) nextBadgeX -= mentionBadgeLayout.width + 6.0 @@ -4433,7 +4403,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.pinnedIconNode.isHidden = false let pinnedIconSize = currentPinnedIconImage.size - let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height) + let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height) strongSelf.pinnedIconNode.frame = pinnedIconFrame nextBadgeX -= pinnedIconSize.width + 6.0 @@ -4450,11 +4420,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset) var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height) actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0)) + actionButtonFrame.origin.y += 4.0 let actionButtonNode: HighlightableButtonNode + var animateActionButtonIn = false if let current = strongSelf.actionButtonNode { actionButtonNode = current } else { + animateActionButtonIn = true actionButtonNode = HighlightableButtonNode() strongSelf.actionButtonNode = actionButtonNode strongSelf.mainContentContainerNode.addSubnode(actionButtonNode) @@ -4483,23 +4456,40 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { actionButtonNode.addSubnode(actionButtonTitleNode) } + actionButtonNode.isUserInteractionEnabled = true actionButtonNode.frame = actionButtonFrame actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size) actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size) + if animateActionButtonIn { + actionButtonNode.alpha = 0.0 + } + transition.updateAlpha(node: actionButtonNode, alpha: 1.0) nextBadgeX -= actionButtonSize.width + 6.0 } else { - if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { - actionButtonTitleNode.removeFromSupernode() - strongSelf.actionButtonTitleNode = nil - } - if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView { - actionButtonBackgroundView.removeFromSuperview() - strongSelf.actionButtonBackgroundView = nil - } if let actionButtonNode = strongSelf.actionButtonNode { - actionButtonNode.removeFromSupernode() + let actionButtonTitleNode = strongSelf.actionButtonTitleNode + let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView + actionButtonNode.isUserInteractionEnabled = false + + strongSelf.actionButtonTitleNode = nil + strongSelf.actionButtonBackgroundView = nil strongSelf.actionButtonNode = nil + + transition.updateAlpha(node: actionButtonNode, alpha: 0.0, completion: { [weak actionButtonNode, weak actionButtonTitleNode, weak actionButtonBackgroundView] _ in + actionButtonTitleNode?.removeFromSupernode() + actionButtonBackgroundView?.removeFromSuperview() + actionButtonNode?.removeFromSupernode() + }) + } else { + if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { + actionButtonTitleNode.removeFromSupernode() + strongSelf.actionButtonTitleNode = nil + } + if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView { + actionButtonBackgroundView.removeFromSuperview() + strongSelf.actionButtonBackgroundView = nil + } } } @@ -5140,7 +5130,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else { strongSelf.updateSeparatorAlpha(transition: transition) } - strongSelf.updateUpperNeighborSeparatorForReveal(strongSelf.isRevealHighlighted, transition: transition) if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { if customMessageListData.hideSeparator { @@ -5336,12 +5325,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size)) - let isRevealHighlighted = !offset.isZero + if !offset.isZero { + self.keepRevealHighlightUntilClosed = true + } + let isRevealHighlighted = !offset.isZero || self.keepRevealHighlightUntilClosed if self.isRevealHighlighted != isRevealHighlighted { self.isRevealHighlighted = isRevealHighlighted self.updateIsHighlighted(transition: transition) } - self.updateUpperNeighborSeparatorForReveal(isRevealHighlighted, transition: transition) + } + + private func clearRevealHighlightIfNeeded(transition: ContainedViewLayoutTransition) { + self.keepRevealHighlightUntilClosed = false + if self.revealOffset.isZero && self.isRevealHighlighted { + self.isRevealHighlighted = false + self.updateIsHighlighted(transition: transition) + } } override public func touchesToOtherItemsPrevented() { @@ -5349,9 +5348,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { item.interaction.setPeerIdWithRevealedOptions(nil, nil) } + self.clearRevealHighlightIfNeeded(transition: .immediate) } override public func revealOptionsInteractivelyOpened() { + self.keepRevealHighlightUntilClosed = true if let item = self.item { switch item.index { case let .chatList(index): @@ -5363,6 +5364,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } override public func revealOptionsInteractivelyClosed() { + self.clearRevealHighlightIfNeeded(transition: .animated(duration: 0.2, curve: .easeInOut)) if let item = self.item { switch item.index { case let .chatList(index): diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index f18aba2e18..3641cf4467 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -7,19 +7,22 @@ public final class Image: Component { public let size: CGSize? public let contentMode: UIImageView.ContentMode public let cornerRadius: CGFloat + public let flipHorizontally: Bool public init( image: UIImage?, tintColor: UIColor? = nil, size: CGSize? = nil, contentMode: UIImageView.ContentMode = .scaleToFill, - cornerRadius: CGFloat = 0.0 + cornerRadius: CGFloat = 0.0, + flipHorizontally: Bool = false ) { self.image = image self.tintColor = tintColor self.size = size self.contentMode = contentMode self.cornerRadius = cornerRadius + self.flipHorizontally = flipHorizontally } public static func ==(lhs: Image, rhs: Image) -> Bool { @@ -38,6 +41,9 @@ public final class Image: Component { if lhs.cornerRadius != rhs.cornerRadius { return false } + if lhs.flipHorizontally != rhs.flipHorizontally { + return false + } return true } @@ -51,7 +57,11 @@ public final class Image: Component { } func update(component: Image, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { - self.image = component.image + if component.flipHorizontally, let cgImage = component.image?.cgImage { + self.image = UIImage(cgImage: cgImage, scale: component.image?.scale ?? 0.0, orientation: .upMirrored) + } else { + self.image = component.image + } self.contentMode = component.contentMode self.clipsToBounds = component.cornerRadius > 0.0 diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index c48f851ca9..36cc4388f6 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -11,15 +11,17 @@ public final class BundleIconComponent: Component { public let scaleFactor: CGFloat public let shadowColor: UIColor? public let shadowBlur: CGFloat + public let flipHorizontally: Bool public let flipVertically: Bool - public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipHorizontally: Bool = false, flipVertically: Bool = false) { self.name = name self.tintColor = tintColor self.maxSize = maxSize self.scaleFactor = scaleFactor self.shadowColor = shadowColor self.shadowBlur = shadowBlur + self.flipHorizontally = flipHorizontally self.flipVertically = flipVertically } @@ -42,6 +44,9 @@ public final class BundleIconComponent: Component { if lhs.shadowBlur != rhs.shadowBlur { return false } + if lhs.flipHorizontally != rhs.flipHorizontally { + return false + } if lhs.flipVertically != rhs.flipVertically { return false } @@ -77,7 +82,9 @@ public final class BundleIconComponent: Component { } }) } - if component.flipVertically, let cgImage = image?.cgImage { + if component.flipHorizontally, let cgImage = image?.cgImage { + self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .upMirrored) + } else if component.flipVertically, let cgImage = image?.cgImage { self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down) } else { self.image = image diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 980ca8e473..7d0c4b173f 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -291,6 +291,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.scrollsToTop = false self.itemNodes = reactions.map { reaction, count in return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count) @@ -897,6 +898,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index a3fcbc3fad..15288011d5 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -104,19 +104,19 @@ public final class DeviceLocationManager: NSObject { self.currentTopMode = topMode if let topMode = topMode { self.log?("setting mode \(topMode)") + switch topMode { + case .preciseForeground: + self.manager.allowsBackgroundLocationUpdates = false + case .preciseAlways: + self.manager.allowsBackgroundLocationUpdates = true + } + if previousTopMode == nil { if !self.requestedAuthorization { self.requestedAuthorization = true self.manager.requestAlwaysAuthorization() } - - switch topMode { - case .preciseForeground: - self.manager.allowsBackgroundLocationUpdates = false - case .preciseAlways: - self.manager.allowsBackgroundLocationUpdates = true - } - + self.manager.startUpdatingLocation() self.manager.startUpdatingHeading() } diff --git a/submodules/Display/Source/ActionSheetControllerNode.swift b/submodules/Display/Source/ActionSheetControllerNode.swift index c27cc399f8..fc4d61c752 100644 --- a/submodules/Display/Source/ActionSheetControllerNode.swift +++ b/submodules/Display/Source/ActionSheetControllerNode.swift @@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + override func didLoad() { + super.didLoad() + + self.scrollNode.view.scrollsToTop = false + } + func performHighlightedAction() { self.itemGroupsContainerNode.performHighlightedAction() } diff --git a/submodules/Display/Source/ActionSheetItemGroupNode.swift b/submodules/Display/Source/ActionSheetItemGroupNode.swift index cee228963b..af9d79fc27 100644 --- a/submodules/Display/Source/ActionSheetItemGroupNode.swift +++ b/submodules/Display/Source/ActionSheetItemGroupNode.swift @@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index cd2a2a5198..613de4eaf7 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -72,8 +72,21 @@ public extension CALayer { func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) { let components = timingFunction.components(separatedBy: "_") - let damping = Float(components[1]) ?? 100.0 - let initialVelocity = Float(components[2]) ?? 0.0 + let mass: Float + let stiffness: Float + let damping: Float + let initialVelocity: Float + if components.count >= 5 { + mass = Float(components[1]) ?? 5.0 + stiffness = Float(components[2]) ?? 900.0 + damping = Float(components[3]) ?? 100.0 + initialVelocity = Float(components[4]) ?? 0.0 + } else { + mass = 5.0 + stiffness = 900.0 + damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0 + initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0 + } let animation = CASpringAnimation(keyPath: keyPath) animation.fromValue = from @@ -83,10 +96,10 @@ public extension CALayer { if let completion = completion { animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) } + animation.mass = CGFloat(mass) + animation.stiffness = CGFloat(stiffness) animation.damping = CGFloat(damping) animation.initialVelocity = CGFloat(initialVelocity) - animation.mass = 5.0 - animation.stiffness = 900.0 animation.duration = animation.settlingDuration animation.timingFunction = CAMediaTimingFunction.init(name: .linear) let k = Float(UIView.animationDurationFactor()) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 17abeb224c..ef5b78b06b 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case easeInOut case easeIn case spring - case customSpring(damping: CGFloat, initialVelocity: CGFloat) + case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat) case custom(Float, Float, Float, Float) public static var slide: ContainedViewLayoutTransitionCurve { @@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve { return CAMediaTimingFunctionName.easeIn.rawValue case .spring: return kCAMediaTimingFunctionSpring - case let .customSpring(damping, initialVelocity): - return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)" + case let .customSpring(mass, stiffness, damping, initialVelocity): + return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)" case .custom: return CAMediaTimingFunctionName.easeInEaseOut.rawValue } @@ -124,8 +124,8 @@ private extension CALayer { let timingFunction: String let mediaTimingFunction: CAMediaTimingFunction? switch curve { - case .spring: - timingFunction = kCAMediaTimingFunctionSpring + case .spring, .customSpring: + timingFunction = curve.timingFunction mediaTimingFunction = nil default: timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 05cd1827a1..9378f7a4ce 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0) self.scroller.isHidden = true self.scroller.delegate = self.wrappedScrollViewDelegate + self.scroller.scrollsToTop = false self.view.addSubview(self.scroller) self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index d8a004fcd0..452418ad57 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController, private var inCallStatusBar: StatusBar? private var updateInCallStatusBarState: CallStatusBarNode? private var globalScrollToTopNode: ScrollToTopNode? + private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews? private var rootContainer: RootContainer? private var rootModalFrame: NavigationModalFrame? private var modalContainers: [NavigationModalContainer] = [] @@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController, } deinit { + self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil) + } + + private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews { + if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews { + return windowScrollToTopProxyViews + } + + let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in + self?.scrollToTop(subject) + }) + self.windowScrollToTopProxyViews = windowScrollToTopProxyViews + return windowScrollToTopProxyViews + } + + private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? { + var view: UIView? = self.view + while let currentView = view { + if currentView.superview === window { + return currentView + } + view = currentView.superview + } + return nil + } + + private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) { + guard let window = self.view.window, let rootContainer = self.rootContainer else { + (self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true + self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil) + return + } + + (self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false + let referenceView = self.windowScrollToTopReferenceView(window: window) + let proxyViews = self.getWindowScrollToTopProxyViews() + switch rootContainer { + case .flat: + let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0) + let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window) + proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView) + case let .split(container): + let frames = container.scrollToTopProxyFrames(layout: layout) + proxyViews.update(window: window, mode: .split( + masterFrame: container.view.convert(frames.master, to: window), + detailFrame: container.view.convert(frames.detail, to: window) + ), referenceView: referenceView) + } } public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { @@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController, } if let globalScrollToTopNode = self.globalScrollToTopNode { - globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1)) + globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0)) } var overlayContainerLayout = layout @@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController, case let .flat(flatContainer): let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) - }, scrollToTop: { [weak self] subject in - self?.scrollToTop(subject) }) if let detailsPlaceholderNode = self.detailsPlaceholderNode { self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode) @@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController, } else { let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) - }, scrollToTop: { [weak self] subject in - self?.scrollToTop(subject) }) if let detailsPlaceholderNode = self.detailsPlaceholderNode { self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode) @@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController, } } + self.updateWindowScrollToTopProxyViews(layout: layout) + self.isUpdatingContainers = false if notifyGlobalOverlayControllersUpdated { diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index f9120ce6fe..7fd3ed95ba 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.scrollNode.view.clipsToBounds = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.tag = 0x5C4011 + self.scrollNode.view.scrollsToTop = false let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in guard let strongSelf = self, !strongSelf.isDismissed else { diff --git a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift index 42192baf34..b3900d15c5 100644 --- a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift @@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop { final class NavigationSplitContainer: ASDisplayNode { private var theme: NavigationControllerTheme - private let masterScrollToTopView: ScrollToTopView - private let detailScrollToTopView: ScrollToTopView private let masterContainer: NavigationContainer private let detailContainer: NavigationContainer private let separator: ASDisplayNode @@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode { self.detailContainer.isInFocus = isInFocus } - init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) { + init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) { self.theme = theme - self.masterScrollToTopView = ScrollToTopView(frame: CGRect()) - self.masterScrollToTopView.action = { - scrollToTop(.master) - } - self.detailScrollToTopView = ScrollToTopView(frame: CGRect()) - self.detailScrollToTopView.action = { - scrollToTop(.detail) - } - self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved) self.masterContainer.clipsToBounds = true @@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode { self.addSubnode(self.masterContainer) self.addSubnode(self.detailContainer) self.addSubnode(self.separator) - self.view.addSubview(self.masterScrollToTopView) - self.view.addSubview(self.detailScrollToTopView) } func hasNonReadyControllers() -> Bool { @@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode { self.separator.backgroundColor = theme.navigationBar.separatorColor } + func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) { + let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) + let detailWidth = layout.size.width - masterWidth + let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0) + + return ( + master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)), + detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight)) + ) + } + func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) { let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) let detailWidth = layout.size.width - masterWidth - self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0)) - self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0)) - transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height))) transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height))) transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height))) diff --git a/submodules/Display/Source/ScrollToTopProxyView.swift b/submodules/Display/Source/ScrollToTopProxyView.swift index ba727f4d11..ee95519900 100644 --- a/submodules/Display/Source/ScrollToTopProxyView.swift +++ b/submodules/Display/Source/ScrollToTopProxyView.swift @@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate { return false } - - func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { - print("scrollViewDidScrollToTop") - } } class ScrollToTopNode: ASDisplayNode { @@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode { }) } } + +final class WindowScrollToTopProxyViews { + enum Mode { + case disabled + case flat(frame: CGRect) + case split(masterFrame: CGRect, detailFrame: CGRect) + } + + private let flatView: ScrollToTopView + private let masterView: ScrollToTopView + private let detailView: ScrollToTopView + + init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) { + self.flatView = ScrollToTopView(frame: CGRect()) + self.masterView = ScrollToTopView(frame: CGRect()) + self.detailView = ScrollToTopView(frame: CGRect()) + + self.flatView.action = { + scrollToTop(.master) + } + self.masterView.action = { + scrollToTop(.master) + } + self.detailView.action = { + scrollToTop(.detail) + } + } + + deinit { + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + } + + func update(window: UIWindow?, mode: Mode, referenceView: UIView?) { + guard let window else { + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + return + } + + switch mode { + case .disabled: + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + case let .flat(frame): + self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView) + self.disable(self.masterView) + self.disable(self.detailView) + case let .split(masterFrame, detailFrame): + self.disable(self.flatView) + self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView) + self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView) + } + } + + private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) { + if let referenceView, referenceView.superview === window { + if view.superview !== window { + view.removeFromSuperview() + } + window.insertSubview(view, aboveSubview: referenceView) + } else if view.superview !== window { + view.removeFromSuperview() + window.addSubview(view) + } + + view.isHidden = false + view.scrollsToTop = true + view.frame = frame + } + + private func disable(_ view: ScrollToTopView) { + view.scrollsToTop = false + view.isHidden = true + view.removeFromSuperview() + } +} diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index d46a545f30..499389abb4 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -108,6 +108,10 @@ swift_library( "//submodules/TelegramUI/Components/StickerPickerScreen", "//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation", "//submodules/TelegramUI/Components/SegmentControlComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index ca955ce133..206563fa1b 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -14,6 +14,8 @@ import SegmentControlComponent import MultilineTextComponent import HexColor import MediaEditor +import GlassBarButtonComponent +import BundleIconComponent private let palleteColors: [UInt32] = [ 0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000, @@ -1819,30 +1821,6 @@ private final class ColorPickerContent: CombinedComponent { } 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: 0xffffff))! - self.cachedCloseImage = closeImage - } - return closeImage - } - var selectedMode: Int = 0 var selectedColor: DrawingColor @@ -1884,8 +1862,8 @@ private final class ColorPickerContent: CombinedComponent { } static var body: Body { - let eyedropperButton = Child(Button.self) - let closeButton = Child(Button.self) + let eyedropperButton = Child(GlassBarButtonComponent.self) + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(MultilineTextComponent.self) let modeControl = Child(SegmentControlComponent.self) @@ -1918,50 +1896,45 @@ private final class ColorPickerContent: CombinedComponent { let sideInset: CGFloat = 16.0 let eyedropperButton = eyedropperButton.update( - component: Button( - content: AnyComponent( - Image(image: state.eyedropperImage) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Media Editor/Eyedropper", tintColor: .white)) ), - action: { [weak component] in - component?.eyedropper() + action: { _ in + component.eyedropper() } - ).minSize(CGSize(width: 30.0, height: 30.0)), - availableSize: CGSize(width: 19.0, height: 19.0), + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(eyedropperButton - .position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0)) + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - eyedropperButton.size.width / 2.0 - 16.0, y: 16.0 + eyedropperButton.size.height / 2.0)) ) let closeButton = closeButton.update( - component: Button( - content: AnyComponent(ZStack([ - AnyComponentWithIdentity( - id: "background", - component: AnyComponent( - BlurredBackgroundComponent( - color: UIColor(rgb: 0x888888, alpha: 0.1) - ) - ) - ), - AnyComponentWithIdentity( - id: "icon", - component: AnyComponent( - Image(image: state.closeImage) - ) - ), - ])), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: .white)) + ), + action: { _ 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)) - .clipsToBounds(true) - .cornerRadius(15.0) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let title = title.update( @@ -1979,12 +1952,11 @@ private final class ColorPickerContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0)) ) - var contentHeight: CGFloat = 58.0 + var contentHeight: CGFloat = 76.0 - //backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6) let modeControl = modeControl.update( component: SegmentControlComponent( theme: SegmentControlComponent.Theme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.07), legacyBackgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), textColor: .white, dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 2678fbe440..aa578ffa70 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -25,6 +25,9 @@ import FastBlur import MediaEditor import StickerPickerScreen import ImageObjectSeparation +import GlassBarButtonComponent +import GlassControls +import BundleIconComponent public struct DrawingResultData { public let data: Data? @@ -363,15 +366,17 @@ private final class ReferenceContentSource: ContextReferenceContentSource { private let sourceView: UIView private let contentArea: CGRect private let customPosition: CGPoint + private let actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition - init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) { + init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint, actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition = .top) { self.sourceView = sourceView self.contentArea = contentArea self.customPosition = customPosition + self.actionsPosition = actionsPosition } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: self.actionsPosition) } } @@ -467,13 +472,11 @@ private let bottomGradientTag = GenericComponentViewTag() private let undoButtonTag = GenericComponentViewTag() private let redoButtonTag = GenericComponentViewTag() private let clearAllButtonTag = GenericComponentViewTag() +private let topButtonsTag = 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() @@ -489,6 +492,12 @@ private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag, private let cancelButtonTag = GenericComponentViewTag() private let doneButtonTag = GenericComponentViewTag() +enum DrawingMode: Int { + case drawing + case sticker + case text +} + private final class DrawingScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -616,7 +625,6 @@ private final class DrawingScreenComponent: CombinedComponent { final class State: ComponentState { enum ImageKey: Hashable { case undo - case redo case done case add case fill @@ -633,8 +641,6 @@ private final class DrawingScreenComponent: CombinedComponent { 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: @@ -652,13 +658,7 @@ private final class DrawingScreenComponent: CombinedComponent { return image } } - - enum Mode { - case drawing - case sticker - case text - } - + private let context: AccountContext private let updateToolState: ActionSlot private let insertEntity: ActionSlot @@ -675,7 +675,7 @@ private final class DrawingScreenComponent: CombinedComponent { private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? private let present: (ViewController) -> Void - var currentMode: Mode + var currentMode: DrawingMode var drawingState: DrawingState var drawingViewState: DrawingView.NavigationState var currentColor: DrawingColor @@ -992,7 +992,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.present(contextController) } - func updateCurrentMode(_ mode: Mode, update: Bool = true) { + func updateCurrentMode(_ mode: DrawingMode, update: Bool = true) { self.currentMode = mode if let selectedEntity = self.selectedEntity { if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity { @@ -1058,15 +1058,13 @@ private final class DrawingScreenComponent: CombinedComponent { let topGradient = Child(BlurredGradientComponent.self) let bottomGradient = Child(BlurredGradientComponent.self) - let undoButton = Child(Button.self) + let undoButton = Child(GlassBarButtonComponent.self) + let redoButton = Child(GlassBarButtonComponent.self) + let clearAllButton = Child(GlassBarButtonComponent.self) + let topButtons = Child(GlassControlPanelComponent.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 mode = Child(ModeComponent.self) let colorButton = Child(ColorSwatchComponent.self) @@ -1082,16 +1080,11 @@ private final class DrawingScreenComponent: CombinedComponent { 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 backButton = Child(GlassBarButtonComponent.self) + let doneButton = Child(GlassBarButtonComponent.self) let textSize = Child(TextSizeSliderComponent.self) - let textCancelButton = Child(Button.self) - let textDoneButton = Child(Button.self) let presetColors: [DrawingColor] = [ DrawingColor(rgb: 0xff453a), @@ -1220,6 +1213,8 @@ private final class DrawingScreenComponent: CombinedComponent { var additionalBottomInset: CGFloat = 0.0 if component.sourceHint == .storyEditor { additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0) + } else { + additionalBottomInset = 8.0 } if let textEntity = state.selectedEntity as? DrawingTextEntity { @@ -1318,8 +1313,8 @@ private final class DrawingScreenComponent: CombinedComponent { ) } - let rightButtonPosition = rightEdge - 24.0 - var offsetX: CGFloat = leftEdge + 24.0 + let rightButtonPosition = rightEdge - 28.0 + var offsetX: CGFloat = leftEdge + 28.0 let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0 let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in @@ -1608,6 +1603,7 @@ private final class DrawingScreenComponent: CombinedComponent { } var hasTopButtons = false + var centerItems: [GlassControlGroupComponent.Item] = [] if let entity = state.selectedEntity { var isFilled: Bool? if let entity = entity as? DrawingSimpleShapeEntity { @@ -1625,12 +1621,13 @@ private final class DrawingScreenComponent: CombinedComponent { hasTopButtons = isFilled != nil || hasFlip - if let isFilled = isFilled { - let fillButton = fillButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(isFilled ? .fill : .stroke)) - ), + if controlsAreVisible { + if let isFilled = isFilled { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("fill"), + content: .customIcon(id: AnyHashable("fill"), component: AnyComponent( + Image(image: state.image(isFilled ? .fill : .stroke), size: CGSize(width: 30.0, height: 30.0)) + ), insets: .zero), action: { [weak state] in guard let state = state else { return @@ -1661,25 +1658,15 @@ private final class DrawingScreenComponent: CombinedComponent { } 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: topInset)) - .appear(.default(scale: true)) - .disappear(.default(scale: true)) - .opacity(!controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) - ) - } - - if hasFlip { - let flipButton = flipButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.flip)) - ), + )) + } + + if hasFlip { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("flip"), + content: .customIcon(id: AnyHashable("flip"), component: AnyComponent( + Image(image: state.image(.flip), size: CGSize(width: 30.0, height: 30.0)) + ), insets: .zero), action: { [weak state] in guard let state = state else { return @@ -1695,17 +1682,8 @@ private final class DrawingScreenComponent: CombinedComponent { } 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: topInset)) - .appear(.default(scale: true)) - .disappear(.default(scale: true)) - .opacity(!controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) - ) + )) + } } } @@ -1730,28 +1708,24 @@ private final class DrawingScreenComponent: CombinedComponent { 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 state.drawingViewState.canZoomOut && !hasTopButtons && controlsAreVisible { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("zoomOut"), + content: .customIcon(id: AnyHashable("zoomOut"), component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + Image(image: state.image(.zoomOut), size: CGSize(width: 24.0, height: 24.0)) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + Text(text: environment.strings.Paint_ZoomOut, font: Font.regular(17.0), color: .white) + )), + ], spacing: 2.0) + ), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)), + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.zoomOut) + } + )) } } if let sizeValue { @@ -1784,111 +1758,170 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0)) .opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0) ) - - let undoButton = undoButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.undo)) - ), - isEnabled: state.drawingViewState.canUndo, - action: { + + var leftItems: [GlassControlGroupComponent.Item] = [] + var rightItems: [GlassControlGroupComponent.Item] = [] + + if !isEditingText && controlsAreVisible { + leftItems.append(GlassControlGroupComponent.Item( + id: "undo", + content: .customIcon(id: "undo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canUndo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0))), insets: .zero), + action: state.drawingViewState.canUndo ? { dismissEyedropper.invoke(Void()) performAction.invoke(.undo) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), - availableSize: CGSize(width: 24.0, height: 24.0), + } : nil + )) + + if state.drawingViewState.canRedo { + leftItems.append(GlassControlGroupComponent.Item( + id: "redo", + content: .customIcon(id: "redo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canRedo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0), flipHorizontally: true)), insets: .zero), + action: state.drawingViewState.canRedo ? { + dismissEyedropper.invoke(Void()) + performAction.invoke(.redo) + } : nil + )) + } + } + + let undoButton = undoButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image(image: state.image(.undo))) + ), + action: { _ in + dismissEyedropper.invoke(Void()) + performAction.invoke(.undo) + }, + tag: undoButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(undoButton - .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset)) + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 16.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) - .opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) + .opacity(0.0) + //.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) ) - let redoButton = redoButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.redo)) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image(image: state.image(.undo), flipHorizontally: true)) ), - action: { + action: { _ in dismissEyedropper.invoke(Void()) performAction.invoke(.redo) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag), - availableSize: CGSize(width: 24.0, height: 24.0), + }, + tag: redoButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.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 && controlsAreVisible ? 1.0 : 0.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) + .opacity(0.0) + //.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0) ) let clearAllButton = clearAllButton.update( - component: Button( - content: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)), - textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil, - textShadowBlur: 2.0 - ) + component: GlassBarButtonComponent( + size: nil, + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "label", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)) + )) ), - isEnabled: state.drawingViewState.canClear, - action: { + action: { _ in dismissEyedropper.invoke(Void()) performAction.invoke(.clear) - } - ).tagged(clearAllButtonTag), + }, + tag: clearAllButtonTag + ), availableSize: CGSize(width: 180.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 || !controlsAreVisible ? 0.0 : 1.0) + .opacity(0.0) + //.opacity(isEditingText || !controlsAreVisible ? 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) - ), + if !isEditingText && controlsAreVisible { + rightItems.append(GlassControlGroupComponent.Item( + id: "clearAll", + content: .text(strings.Paint_Clear), + action: state.drawingViewState.canClear ? { + dismissEyedropper.invoke(Void()) + performAction.invoke(.clear) + } : nil + )) + } + + if isEditingText { + leftItems.append(GlassControlGroupComponent.Item( + id: "cancel", + content: .text(strings.Common_Cancel), action: { [weak state] in if let entity = state?.selectedEntity as? DrawingTextEntity { endEditingTextEntityView.invoke((entity.uuid, 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) - ), + )) + + rightItems.append(GlassControlGroupComponent.Item( + id: "done", + content: .text(strings.Common_Done), action: { [weak state] in if let entity = state?.selectedEntity as? DrawingTextEntity { endEditingTextEntityView.invoke((entity.uuid, false)) } } + )) + } + + let topButtons = topButtons.update( + component: GlassControlPanelComponent( + theme: defaultDarkPresentationTheme, + leftItem: leftItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: leftItems, + background: .panel + ), + centralItem: centerItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: centerItems, + background: .panel + ), + rightItem: rightItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: true, + tag: topButtonsTag ), - availableSize: CGSize(width: 100.0, height: 30.0), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: 44.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) + context.add(topButtons + .position(CGPoint(x: context.availableSize.width / 2.0, y: topInset)) + //.opacity(isEditingText ? 1.0 : 0.0) ) var color: DrawingColor? @@ -1930,7 +1963,7 @@ private final class DrawingScreenComponent: CombinedComponent { 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 - additionalBottomInset)) + .position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 6.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) .opacity(controlsAreVisible ? 1.0 : 0.0) @@ -1979,7 +2012,7 @@ private final class DrawingScreenComponent: CombinedComponent { 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 - additionalBottomInset)) + .position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 6.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) .cornerRadius(12.0) @@ -1987,27 +2020,38 @@ private final class DrawingScreenComponent: CombinedComponent { ) let doneButton = doneButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.done)) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: UIColor(rgb: 0x0088ff), + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) ), - action: { [weak state] in + 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), + }, + tag: doneButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) - var doneButtonPosition = 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) + var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 14.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { doneButtonPosition.x = doneButtonPosition.x - 2.0 if case .regular = environment.metrics.widthClass { doneButtonPosition.x -= 20.0 } - doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + 5.0 + } else { + doneButtonPosition.x = doneButtonPosition.x - 12.0 + doneButtonPosition.y -= 3.0 - UIScreenPixel } context.add(doneButton .position(doneButtonPosition) @@ -2026,117 +2070,82 @@ private final class DrawingScreenComponent: CombinedComponent { }) .opacity(controlsAreVisible ? 1.0 : 0.0) ) - - 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 { + + let mode = mode.update( + component: ModeComponent( + isTablet: false, + strings: environment.strings, + tintColor: .white, + availableModes: [.drawing, .sticker, .text], + currentMode: state.currentMode, + updatedMode: { [weak state] mode in + if let state { dismissEyedropper.invoke(Void()) - state.updateBrushSize(size) - if state.selectedEntity == nil { - previewBrushSize.invoke(size) + switch mode { + case .drawing: + state.updateCurrentMode(.drawing) + case .sticker: + state.presentStickerPicker() + case .text: + state.addTextEntity() } } }, - sizeReleased: { - previewBrushSize.invoke(nil) - } + tag: modeTag ), - availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height), + availableSize: CGSize(width: context.availableSize.width - 66.0 * 2.0, height: 44.0), transition: context.transition ) - var modeAndSizePosition = 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 modePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - mode.size.height / 2.0 - 9.0) if component.sourceHint == .storyEditor { - modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset + modePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + mode.size.height / 2.0) + controlsBottomInset + } else { + modePosition.y += 4.0 } - context.add(modeAndSize - .position(modeAndSizePosition) + context.add(mode + .position(modePosition) .opacity(controlsAreVisible ? 1.0 : 0.0) ) - var animatingOut = false - if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition { - animatingOut = true - } - - if animatingOut && component.sourceHint == .storyEditor { - - } else { - 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()) - } + let backButton = backButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Close", tintColor: .white) + ) + ), + action: { [weak state] _ in + if let state { + dismissEyedropper.invoke(Void()) + state.saveToolState() + dismiss.invoke(Void()) } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag), - availableSize: CGSize(width: 33.0, height: 33.0), - transition: .immediate - ) - var backButtonPosition = 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) - if component.sourceHint == .storyEditor { - backButtonPosition.x = backButtonPosition.x + 2.0 - if case .regular = environment.metrics.widthClass { - backButtonPosition.x += 20.0 - } - backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + }, + tag: cancelButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 14.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) + if component.sourceHint == .storyEditor { + backButtonPosition.x = backButtonPosition.x + 2.0 + if case .regular = environment.metrics.widthClass { + backButtonPosition.x += 20.0 } - context.add(backButton - .position(backButtonPosition) - .opacity(controlsAreVisible ? 1.0 : 0.0) - ) + backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + 5.0 + } else { + backButtonPosition.x = backButtonPosition.x + 12.0 + backButtonPosition.y -= 3.0 - UIScreenPixel } + context.add(backButton + .position(backButtonPosition) + .opacity(controlsAreVisible ? 1.0 : 0.0) + ) return context.availableSize } @@ -2224,22 +2233,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.performAction.connect { [weak self] action in if let self { if case .clear = action { - let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - - self?._drawingView?.performAction(action) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.controller?.present(actionSheet, in: .window(.root)) + let sourceView: UIView + if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = topButtonsView.rightItemView, let clearAllView = rightItemView.itemView(id: AnyHashable("clearAll")) { + sourceView = clearAllView + } else { + sourceView = self.view + } + + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: self.presentationData.strings.Paint_ClearConfirm, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] f in + f.dismissWithResult(.default) + self?._drawingView?.performAction(.clear) + })) + ] + let presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(), actionsPosition: .bottom)), items: .single(ContextController.Items(content: .list(items)))) + self.controller?.present(contextController, in: .window(.root)) } else { self._drawingView?.performAction(action) } @@ -2403,22 +2414,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U 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)) + let sourceView = strongSelf.componentHost.findTaggedView(tag: cancelButtonTag) ?? strongSelf.view + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] f in + f.dismissWithResult(.default) + self?.controller?.requestDismiss() + })) + ] + let presentationData = strongSelf.presentationData.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint())), items: .single(ContextController.Items(content: .list(items)))) + strongSelf.controller?.present(contextController, in: .window(.root)) } else { strongSelf.controller?.requestDismiss() } @@ -2497,17 +2504,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U 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 topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View { + topButtonsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + if let leftItemView = topButtonsView.leftItemView { + leftItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + leftItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let rightItemView = topButtonsView.rightItemView { + rightItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + rightItemView.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 view = self.componentHost.findTaggedView(tag: modeTag) { + view.layer.animateAlpha(from: 0.0, 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) + if let view = self.componentHost.findTaggedView(tag: addButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } var delay: Double = 0.0 for tag in colorTags { @@ -2520,6 +2534,15 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U 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) } + + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: doneButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } } func animateOut(completion: @escaping () -> Void) { @@ -2536,43 +2559,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } 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 topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View { + topButtonsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + + if let view = topButtonsView.leftItemView { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } + if let view = topButtonsView.rightItemView { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } } 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: addButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) } 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) @@ -2596,12 +2600,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U }) } - if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View { - view.animateOut() + if let view = self.componentHost.findTaggedView(tag: modeTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } - if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } + + if let view = self.componentHost.findTaggedView(tag: doneButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) } } diff --git a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift index a8822e5230..0872e00b73 100644 --- a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift +++ b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift @@ -2,95 +2,123 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents -import TelegramCore -import SegmentedControlNode +import MultilineTextComponent +import TelegramPresentationData +import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer -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 let buttonSize = CGSize(width: 55.0, height: 44.0) +private let tabletButtonSize = CGSize(width: 55.0, height: 44.0) + +extension DrawingMode { + func title(strings: PresentationStrings) -> String { + switch self { + case .drawing: + return strings.Paint_Draw + case .sticker: + return strings.Paint_Sticker + case .text: + return strings.Paint_Text + } + } } -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)) -} - -final class ModeAndSizeComponent: Component { - let values: [String] - let sizeValue: CGFloat - let isEditing: Bool - let isEnabled: Bool - let rightInset: CGFloat +final class ModeComponent: Component { + let isTablet: Bool + let strings: PresentationStrings + let tintColor: UIColor + let availableModes: [DrawingMode] + let currentMode: DrawingMode + let updatedMode: (DrawingMode) -> Void 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 + init( + isTablet: Bool, + strings: PresentationStrings, + tintColor: UIColor, + availableModes: [DrawingMode], + currentMode: DrawingMode, + updatedMode: @escaping (DrawingMode) -> Void, + tag: AnyObject? + ) { + self.isTablet = isTablet + self.strings = strings + self.tintColor = tintColor + self.availableModes = availableModes + self.currentMode = currentMode + self.updatedMode = updatedMode 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 { + static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { + if lhs.isTablet != rhs.isTablet { return false } - if lhs.sizeValue != rhs.sizeValue { + if lhs.strings !== rhs.strings { return false } - if lhs.isEditing != rhs.isEditing { + if lhs.tintColor != rhs.tintColor { return false } - if lhs.isEnabled != rhs.isEnabled { + if lhs.availableModes != rhs.availableModes { return false } - if lhs.rightInset != rhs.rightInset { - return false - } - if lhs.selectedIndex != rhs.selectedIndex { + if lhs.currentMode != rhs.currentMode { return false } return true } - - final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { - private let backgroundNode: NavigationBackgroundNode - private let node: SegmentedControlNode + + final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } - private var knob: UIImageView + private struct LayoutData { + var containerSize: CGSize + var selectedFrame: CGRect + var cornerRadius: CGFloat? + var isTablet: Bool + } - private let maskLayer = SimpleShapeLayer() + private var component: ModeComponent? + private var state: EmptyComponentState? - private var isEditing: Bool? - private var isControlEnabled: Bool? - private var sliderWidth: CGFloat = 0.0 + final class ItemView: HighlightTrackingButton { + init() { + super.init(frame: .zero) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize { + let title = NSMutableAttributedString(string: value, font: Font.with(size: 15.0, design: .regular, weight: .medium), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center) + self.setAttributedTitle(title, for: .normal) + self.sizeToFit() + return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: buttonSize.height) + } + } - fileprivate var updated: (CGFloat) -> Void = { _ in } - fileprivate var released: () -> Void = { } + private var backgroundView = UIView() + private var backgroundContainer = GlassBackgroundContainerView() + + private var liquidLensView: LiquidLensView? + private let scrollView = ScrollView() + private let selectedScrollView = UIView() + private var ignoreScrolling = false + private var layoutData: LayoutData? + + private var itemViews: [AnyHashable: ItemView] = [:] + private var selectedItemViews: [AnyHashable: ItemView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? - private var component: ModeAndSizeComponent? public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject @@ -102,164 +130,350 @@ final class ModeAndSizeComponent: Component { } 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.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.09) + self.backgroundView.layer.cornerRadius = 22.0 + self.layer.allowsGroupOpacity = true - - self.addSubview(self.backgroundNode.view) - self.addSubview(self.node.view) - self.addSubview(self.knob) - self.backgroundNode.layer.mask = self.maskLayer + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = true + self.scrollView.delegate = self + self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + return self.scrollView.contentOffset.x > .ulpOfOne + } - let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) - pressGestureRecognizer.minimumPressDuration = 0.01 - pressGestureRecognizer.delegate = self - self.addGestureRecognizer(pressGestureRecognizer) + self.selectedScrollView.clipsToBounds = true + self.selectedScrollView.isUserInteractionEnabled = false - let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) - panGestureRecognizer.delegate = self - self.addGestureRecognizer(panGestureRecognizer) + self.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.backgroundContainer) } 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) + private var animatedOut = false + func animateOutToEditor(transition: ComponentTransition) { + self.animatedOut = true + + transition.setAlpha(view: self.backgroundView, alpha: 0.0) + transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0)) } - @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 + func animateInFromEditor(transition: ComponentTransition) { + self.animatedOut = false + + transition.setAlpha(view: self.backgroundView, alpha: 1.0) + transition.setSublayerTransform(view: self, transform: CATransform3DIdentity) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.backgroundView.frame.contains(point) + } + + private func item(at point: CGPoint, in view: UIView) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + let itemFrame = itemView.convert(itemView.bounds, to: view) + if itemFrame.contains(point) { + return id + } else { + let distance = abs(point.x - itemFrame.midX) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateScrolling(transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + return false + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + let translation = recognizer.translation(in: self) + if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) { + self.selectionGestureState = nil + recognizer.state = .cancelled + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + return + } + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location, in: self) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) } - let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) - self.updated(value) case .ended, .cancelled: - self.released() + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else { + return + } + component.updatedMode(item) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } default: break } } - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled { - return isEditing && isControlEnabled + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else { + return + } + + let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX + var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size) + if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet { + lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0) + } + + if layoutData.isTablet { + lensSelection.size.width = layoutData.containerSize.width } else { - return false + lensSelection.origin.x -= contentOffsetX + lensSelection.origin.y = 0.0 + lensSelection.size.height = layoutData.containerSize.height } + + let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width) + transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize)) + transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize)) + + liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate) + + self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne } - - 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: ComponentTransition) -> 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) + func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let isTablet = component.isTablet + + let liquidLensView: LiquidLensView + if let current = self.liquidLensView { + liquidLensView = current + } else { + liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer) + self.liquidLensView = liquidLensView + self.backgroundContainer.contentView.addSubview(liquidLensView) + liquidLensView.contentView.addSubview(self.scrollView) + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + tabSelectionRecognizer.delegate = self + tabSelectionRecognizer.cancelsTouchesInView = false + self.tabSelectionRecognizer = tabSelectionRecognizer + liquidLensView.addGestureRecognizer(tabSelectionRecognizer) + } + if self.scrollView.superview == nil { + liquidLensView.contentView.addSubview(self.scrollView) + } + if self.selectedScrollView.superview == nil { + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + } + + self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) + + var inset: CGFloat = 23.0 + let spacing: CGFloat + if isTablet { + spacing = 9.0 + } else { + if availableSize.width < 200.0 { + inset = 20.0 + spacing = 24.0 } 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) + spacing = 30.0 + } + } + + var i = 0 + var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize) + var selectedFrame = itemFrame + + var validKeys: Set = Set() + for mode in component.availableModes { + let id = mode.rawValue + validKeys.insert(id) + + let itemView: ItemView + let selectedItemView: ItemView + if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] { + itemView = current + selectedItemView = currentSelected + } else { + itemView = ItemView() + itemView.isUserInteractionEnabled = false + self.itemViews[id] = itemView + + selectedItemView = ItemView() + selectedItemView.isUserInteractionEnabled = false + self.selectedItemViews[id] = selectedItemView + } + if itemView.superview !== self.scrollView { + self.scrollView.addSubview(itemView) + } + if selectedItemView.superview !== self.selectedScrollView { + self.selectedScrollView.addSubview(selectedItemView) + } + + let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) + itemView.bounds = CGRect(origin: .zero, size: itemSize) + + let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor) + selectedItemView.bounds = CGRect(origin: .zero, size: itemSize) + + itemFrame = CGRect(origin: itemFrame.origin, size: itemSize) + + if mode == component.currentMode { + selectedFrame = itemFrame + } + + if isTablet { + itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) + selectedItemView.center = itemView.center + itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing) + } else { + itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + selectedItemView.center = itemView.center + itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0) + } + i += 1 + } + + var removeKeys: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validKeys.contains(id) { + removeKeys.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + + if let selectedItemView = self.selectedItemViews[id] { + transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in + selectedItemView.removeFromSuperview() + }) } } - transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath) } - - transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize)) + for id in removeKeys { + self.itemViews.removeValue(forKey: id) + self.selectedItemViews.removeValue(forKey: id) + } - transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + let totalSize: CGSize + let size: CGSize + let contentSize: CGSize + var cornerRadius: CGFloat? + if isTablet { + totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) + size = CGSize(width: availableSize.width, height: availableSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize)) + contentSize = totalSize + cornerRadius = 20.0 + } else { + size = CGSize(width: availableSize.width, height: buttonSize.height) + totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height) + let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize)) + contentSize = totalSize + } - if let screenTransition = transition.userData(DrawingScreenTransition.self) { - switch screenTransition { - case .animateIn: - self.animateIn() - case .animateOut: - self.animateOut() + let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) + transition.setFrame(view: self.backgroundContainer, frame: containerFrame) + transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) + + let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size) + transition.setFrame(view: self.scrollView, frame: scrollViewFrame) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne + + self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet) + + self.ignoreScrolling = true + var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size) + let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width) + let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size + if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem { + let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25) + if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX { + scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead + } + if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead { + scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead } } + scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX)) + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) return size } } - + func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 88e6dc1925..ccf2c4a8dd 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -675,6 +675,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll super.didLoad() self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:))) backwardLongPressGestureRecognizer.minimumPressDuration = 0.3 @@ -1356,7 +1357,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll speed: settingsButtonState.speed, quality: settingsButtonState.quality, isOpen: false - ))), + )), insets: .zero), action: { [weak self] in guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else { return @@ -1819,12 +1820,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 { preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in if let strongSelf = self { - let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in + let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - strongSelf.controllerInteraction?.presentController(openInController, nil) + strongSelf.controllerInteraction?.pushController(openInController) } })) } else { @@ -2124,12 +2125,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if availableOpenInOptions(context: self.context, item: item).count > 1 { preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in if let strongSelf = self { - let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in + let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - strongSelf.controllerInteraction?.presentController(openInController, nil) + strongSelf.controllerInteraction?.pushController(openInController) } })) } else { diff --git a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift index 1f7e9247e8..1c5ef1273b 100644 --- a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift @@ -83,6 +83,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel self.scrollNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 5919706cff..a70cc1f360 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -419,6 +419,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in adView.removeFromSuperview() + if self.adView.view === adView { + self.adView = ComponentView() + } Queue.mainQueue().after(0.1) { adView.layer.removeAllAnimations() } @@ -444,7 +447,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN return result } } - if let adView = self.adView.view, adView.frame.contains(point) { + if let adView = self.adView.view, adView.superview === self.view, !self.isAnimatingOut, adView.frame.contains(point) { return super.hitTest(point, with: event) } return nil @@ -3892,12 +3895,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !presentationData.theme.overallDarkAppearance { presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) } - let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) } }) - controller.present(actionSheet, in: .window(.root)) + controller.push(actionSheet) } }))) break diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index a00b0f3073..31ea269639 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -110,6 +110,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 95f7b281d0..71485e43cd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -140,6 +140,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { self.addSubnode(self.navigationBar) self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.navigationBar.back = navigateBack self.navigationBar.share = { [weak self] in @@ -1789,12 +1790,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { private func openUrlIn(_ url: InstantPageUrlItem) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } }) - self.present(actionSheet, nil) + self.pushController(actionSheet) } private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] { diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index 50c1222805..c60553a9c8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -52,6 +52,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 5f3ef466a0..b630df7718 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -410,14 +410,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let leftInset: CGFloat = 65.0 + params.leftInset - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 13.0 - case .legacy: - verticalInset = 11.0 - } - + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 2.0 let separatorHeight = UIScreenPixel diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index cbd9e07683..0b641c47f8 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/TelegramUI/Components/HorizontalTabsComponent", ], visibility = [ diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 435138cb45..9b83218c2d 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -689,6 +689,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } + public func itemNode(forTag tag: ItemListItemTag) -> ListViewItemNode? { + var result: ListViewItemNode? + self.forEachItemNode { itemNode in + if result == nil, let taggedItemNode = itemNode as? ItemListItemNode, let itemTag = taggedItemNode.tag, itemTag.isEqual(to: tag) { + result = itemNode + } + } + return result + } + public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve) } diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 76901d6706..6b9efaf342 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -6,6 +6,8 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import MergeLists +import ComponentFlow +import GlassControls public protocol ItemListHeaderItemNode: AnyObject { func updateTheme(theme: PresentationTheme) @@ -255,7 +257,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private var emptyStateItem: ItemListControllerEmptyStateItem? private var emptyStateNode: ItemListControllerEmptyStateItemNode? - private var toolbarNode: ToolbarNode? + private var toolbar: ComponentView? private var searchItem: ItemListControllerSearch? private var searchNode: ItemListControllerSearchNode? @@ -654,7 +656,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.bottom = max(insets.bottom, additionalInsets.bottom) let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } @@ -666,60 +668,165 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.listNodeContainer.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) } - if let toolbarItem = self.toolbarItem { - var tabBarHeight: CGFloat - let bottomInset: CGFloat = insets.bottom - if !layout.safeInsets.left.isZero { - tabBarHeight = 34.0 + bottomInset - insets.bottom += 34.0 + if let toolbarData = self.toolbarItem, let theme = self.theme { + var panelsBottomInset: CGFloat = layout.insets(options: []).bottom + if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 + } + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - tabBarHeight = 49.0 + bottomInset - insets.bottom += 49.0 + panelsBottomInset = max(panelsBottomInset, 8.0) } - let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + let sideInset: CGFloat = 20.0 + let toolbarHeight = 44.0 + let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight)) - if let toolbarNode = self.toolbarNode { - transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition) - } else if let theme = self.theme { - let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true) - toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate) - self.addSubnode(toolbarNode) - self.toolbarNode = toolbarNode - if case let .animated(duration, curve) = transition { - toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true) + let toolbar: ComponentView + var toolbarTransition = ComponentTransition(transition) + if let current = self.toolbar { + toolbar = current + } else { + toolbar = ComponentView() + self.toolbar = toolbar + toolbarTransition = .immediate + } + + let _ = toolbar.update( + transition: toolbarTransition, + component: AnyComponent(GlassControlPanelComponent( + theme: theme, + leftItem: toolbarData.toolbar.leftAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "left_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + toolbarData.actions[0].action() + } : nil + )], + background: .panel + ) + }, + centralItem: toolbarData.toolbar.middleAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + if toolbarData.actions.count == 1 { + toolbarData.actions[0].action() + } else if toolbarData.actions.count == 3 { + toolbarData.actions[1].action() + } + } : nil + )], + background: .panel + ) + }, + rightItem: toolbarData.toolbar.rightAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + if toolbarData.actions.count == 2 { + toolbarData.actions[1].action() + } else if toolbarData.actions.count == 3 { + toolbarData.actions[2].action() + } + } : nil + )], + background: .panel + ) + }, + centerAlignmentIfPossible: true + )), + environment: {}, + containerSize: toolbarFrame.size + ) + + if let toolbarView = toolbar.view { + if toolbarView.superview == nil { + self.view.addSubview(toolbarView) + toolbarView.alpha = 0.0 } + toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame) + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0) } - - self.toolbarNode?.left = { - toolbarItem.actions[0].action() - } - self.toolbarNode?.right = { - if toolbarItem.actions.count == 2 { - toolbarItem.actions[1].action() - } else if toolbarItem.actions.count == 3 { - toolbarItem.actions[2].action() - } - } - self.toolbarNode?.middle = { - if toolbarItem.actions.count == 1 { - toolbarItem.actions[0].action() - } else if toolbarItem.actions.count == 3 { - toolbarItem.actions[1].action() - } - } - } else if let toolbarNode = self.toolbarNode { - self.toolbarNode = nil - if case let .animated(duration, curve) = transition { - toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in - toolbarNode?.removeFromSupernode() + } else if let toolbar = self.toolbar { + self.toolbar = nil + if let toolbarView = toolbar.view { + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in + toolbarView?.removeFromSuperview() }) - } else { - toolbarNode.removeFromSupernode() } } + +// if let toolbarItem = self.toolbarItem { +// var tabBarHeight: CGFloat +// let bottomInset: CGFloat = insets.bottom +// if !layout.safeInsets.left.isZero { +// tabBarHeight = 34.0 + bottomInset +// insets.bottom += 34.0 +// } else { +// tabBarHeight = 49.0 + bottomInset +// insets.bottom += 49.0 +// } +// +// let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) +// +// if let toolbarNode = self.toolbarNode { +// transition.updateFrame(node: toolbarNode, frame: toolbarFrame) +// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition) +// } else if let theme = self.theme { +// let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true) +// toolbarNode.frame = toolbarFrame +// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate) +// self.addSubnode(toolbarNode) +// self.toolbarNode = toolbarNode +// if case let .animated(duration, curve) = transition { +// toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true) +// } +// } +// +// self.toolbarNode?.left = { +// toolbarItem.actions[0].action() +// } +// self.toolbarNode?.right = { +// if toolbarItem.actions.count == 2 { +// toolbarItem.actions[1].action() +// } else if toolbarItem.actions.count == 3 { +// toolbarItem.actions[2].action() +// } +// } +// self.toolbarNode?.middle = { +// if toolbarItem.actions.count == 1 { +// toolbarItem.actions[0].action() +// } else if toolbarItem.actions.count == 3 { +// toolbarItem.actions[1].action() +// } +// } +// } else if let toolbarNode = self.toolbarNode { +// self.toolbarNode = nil +// if case let .animated(duration, curve) = transition { +// toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in +// toolbarNode?.removeFromSupernode() +// }) +// } else { +// toolbarNode.removeFromSupernode() +// } +// } if let headerItemNode = self.headerItemNode { let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition) @@ -977,7 +1084,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.bottom = footerHeight let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index bbf501135c..34e563f6f2 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -164,7 +164,7 @@ public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ par } public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool { - return params.width >= 350.0 + return params.width >= 320.0 } public final class ItemListPresentationData: Equatable { diff --git a/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift b/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift index 73439eac3b..9082e1c502 100644 --- a/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift +++ b/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift @@ -74,14 +74,20 @@ public struct ItemListRevealOption: Equatable { } private let titleFont = Font.regular(11.0) +private let iconlessTitleFont = Font.regular(13.0) private let optionSpacing: CGFloat = 10.0 private let optionEdgeInset: CGFloat = 10.0 private let optionTitleSpacing: CGFloat = 4.0 private let optionRevealStartOverlap: CGFloat = 12.0 private let optionRevealEndDistance: CGFloat = 10.0 -private let optionExpandedActivationWidthFactor: CGFloat = 5.0 +private let optionExpandedActivationWidthFactor: CGFloat = 3.0 private let optionExpandedTransitionDistance: CGFloat = 16.0 +private let optionIconlessTitleHorizontalInset: CGFloat = 10.0 +private let optionIconAnimationResponse: CGFloat = 18.0 +private let optionIconAnimationSnapDistance: CGFloat = 0.5 +private let optionIconAnimationSnapSize: CGFloat = 0.5 +private let optionIconAnimationSnapAlpha: CGFloat = 0.01 private struct ItemListRevealOptionLayoutMetrics { let shapeSize: CGSize @@ -122,6 +128,10 @@ private func clampToUnitInterval(_ value: CGFloat) -> CGFloat { return max(0.0, min(1.0, value)) } +private func frameCenter(_ frame: CGRect) -> CGPoint { + return CGPoint(x: frame.midX, y: frame.midY) +} + private final class ItemListRevealOptionNode: ASDisplayNode { private let contentContainerNode: ASDisplayNode private let backgroundNode: ASDisplayNode @@ -131,10 +141,21 @@ private final class ItemListRevealOptionNode: ASDisplayNode { private let animationNode: SimpleAnimationNode? private let enableAnimations: Bool + private let displaysTitleInsidePill: Bool private var animationScale: CGFloat = 1.0 private var animationNodeOffset: CGFloat = 0.0 private var animationNodeFlip = false + + private var iconAnimationLink: SharedDisplayLinkDriver.Link? + private weak var manuallyAnimatedIconNode: ASDisplayNode? + private var currentIconCenter: CGPoint? + private var targetIconCenter: CGPoint? + private var currentIconSize: CGSize? + private var targetIconSize: CGSize? + private var currentTitleAlpha: CGFloat? + private var targetTitleAlpha: CGFloat? + private var didApplyLayout = false var isExpanded: Bool = false @@ -150,7 +171,15 @@ private final class ItemListRevealOptionNode: ASDisplayNode { self.titleNode = ASTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail - self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: textColor) + + let displaysTitleInsidePill: Bool + if case .none = icon { + displaysTitleInsidePill = true + } else { + displaysTitleInsidePill = false + } + self.displaysTitleInsidePill = displaysTitleInsidePill + self.titleNode.attributedText = NSAttributedString(string: title, font: displaysTitleInsidePill ? iconlessTitleFont : titleFont, textColor: displaysTitleInsidePill ? iconColor : textColor) self.enableAnimations = enableAnimations @@ -201,6 +230,10 @@ private final class ItemListRevealOptionNode: ASDisplayNode { self.highlightNode.backgroundColor = color.withMultipliedBrightnessBy(0.9) } + deinit { + self.stopManualIconAnimation() + } + func setHighlighted(_ highlighted: Bool) { if highlighted { self.contentContainerNode.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) @@ -214,25 +247,150 @@ private final class ItemListRevealOptionNode: ASDisplayNode { func resetAnimation() { self.animationNode?.reset() + self.stopManualIconAnimation() + } + + private func currentIconPresentationFrame(iconNode: ASDisplayNode) -> CGRect { + return iconNode.layer.presentation()?.frame ?? iconNode.frame + } + + private func currentTitlePresentationAlpha() -> CGFloat { + if let presentation = self.titleNode.layer.presentation() { + return CGFloat(presentation.opacity) + } else { + return self.titleNode.alpha + } + } + + private func applyManualIconState(iconNode: ASDisplayNode, center: CGPoint, size: CGSize, titleAlpha: CGFloat) { + iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(center.x - size.width / 2.0), y: floorToScreenPixels(center.y - size.height / 2.0)), size: size) + self.titleNode.alpha = titleAlpha + } + + private func isManualIconAnimationAtTarget(center: CGPoint, size: CGSize, titleAlpha: CGFloat) -> Bool { + guard let targetIconCenter = self.targetIconCenter, let targetIconSize = self.targetIconSize, let targetTitleAlpha = self.targetTitleAlpha else { + return true + } + let centerDeltaX = targetIconCenter.x - center.x + let centerDeltaY = targetIconCenter.y - center.y + let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY) + let sizeDistance = max(abs(targetIconSize.width - size.width), abs(targetIconSize.height - size.height)) + let alphaDistance = abs(targetTitleAlpha - titleAlpha) + return centerDistance <= optionIconAnimationSnapDistance && sizeDistance <= optionIconAnimationSnapSize && alphaDistance <= optionIconAnimationSnapAlpha + } + + private func stopManualIconAnimation() { + self.iconAnimationLink?.isPaused = true + self.iconAnimationLink?.invalidate() + self.iconAnimationLink = nil + self.manuallyAnimatedIconNode = nil + self.currentIconCenter = nil + self.targetIconCenter = nil + self.currentIconSize = nil + self.targetIconSize = nil + self.currentTitleAlpha = nil + self.targetTitleAlpha = nil + } + + private func updateManualIconAnimation(iconNode: ASDisplayNode, targetFrame: CGRect, targetTitleAlpha: CGFloat, forceImmediate: Bool) { + iconNode.layer.removeAnimation(forKey: "position") + iconNode.layer.removeAnimation(forKey: "bounds") + self.titleNode.layer.removeAnimation(forKey: "opacity") + + let targetCenter = frameCenter(targetFrame) + let targetSize = targetFrame.size + + if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil || self.currentIconSize == nil || self.currentTitleAlpha == nil { + let currentFrame = self.currentIconPresentationFrame(iconNode: iconNode) + self.currentIconCenter = frameCenter(currentFrame) + self.currentIconSize = currentFrame.size + self.currentTitleAlpha = self.currentTitlePresentationAlpha() + self.manuallyAnimatedIconNode = iconNode + } + + self.targetIconCenter = targetCenter + self.targetIconSize = targetSize + self.targetTitleAlpha = targetTitleAlpha + + if forceImmediate { + self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha) + self.stopManualIconAnimation() + return + } + + if let currentIconCenter = self.currentIconCenter, let currentIconSize = self.currentIconSize, let currentTitleAlpha = self.currentTitleAlpha, self.isManualIconAnimationAtTarget(center: currentIconCenter, size: currentIconSize, titleAlpha: currentTitleAlpha) { + self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha) + self.stopManualIconAnimation() + return + } + + if self.iconAnimationLink == nil { + self.iconAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + self?.tickManualIconAnimation(deltaTime: deltaTime) + }) + self.iconAnimationLink?.isPaused = false + } + } + + private func tickManualIconAnimation(deltaTime: CGFloat) { + guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter, let currentIconSize = self.currentIconSize, let targetIconSize = self.targetIconSize, let currentTitleAlpha = self.currentTitleAlpha, let targetTitleAlpha = self.targetTitleAlpha else { + self.stopManualIconAnimation() + return + } + + let clampedDeltaTime = min(0.05, max(0.0, deltaTime)) + let progress = 1.0 - exp(-clampedDeltaTime * optionIconAnimationResponse) + let updatedCenter = CGPoint( + x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress, + y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress + ) + let updatedSize = CGSize( + width: currentIconSize.width + (targetIconSize.width - currentIconSize.width) * progress, + height: currentIconSize.height + (targetIconSize.height - currentIconSize.height) * progress + ) + let updatedTitleAlpha = currentTitleAlpha + (targetTitleAlpha - currentTitleAlpha) * progress + + if self.isManualIconAnimationAtTarget(center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha) { + self.applyManualIconState(iconNode: iconNode, center: targetIconCenter, size: targetIconSize, titleAlpha: targetTitleAlpha) + self.stopManualIconAnimation() + } else { + self.currentIconCenter = updatedCenter + self.currentIconSize = updatedSize + self.currentTitleAlpha = updatedTitleAlpha + self.applyManualIconState(iconNode: iconNode, center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha) + } } func updateLayout(isLeft: Bool, isPrimary: Bool, metrics: ItemListRevealOptionLayoutMetrics, revealProgress: CGFloat, overswipeProgress: CGFloat, expandedProgress: CGFloat, isStretched: Bool, isExpanded: Bool, transition: ContainedViewLayoutTransition) { + let didApplyLayout = self.didApplyLayout let bounds = CGRect(origin: CGPoint(), size: self.bounds.size) transition.updateFrame(node: self.contentContainerNode, frame: bounds) - let contentHeight = metrics.contentHeight - let shapeY = floor((bounds.height - contentHeight) / 2.0) + let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude)) + let pillSize: CGSize + if self.displaysTitleInsidePill { + let pillWidth = max(metrics.shapeSize.width, min(metrics.slotWidth, titleSize.width + optionIconlessTitleHorizontalInset * 2.0)) + pillSize = CGSize(width: pillWidth, height: metrics.shapeSize.height) + } else { + pillSize = metrics.shapeSize + } + let shapeY: CGFloat + if self.displaysTitleInsidePill { + shapeY = floor((bounds.height - pillSize.height) / 2.0) + } else { + shapeY = floor((bounds.height - metrics.contentHeight) / 2.0) + } let shapeFrameX: CGFloat if isStretched { - shapeFrameX = isLeft ? 0.0 : bounds.width - metrics.shapeSize.width + shapeFrameX = isLeft ? 0.0 : bounds.width - pillSize.width } else { - shapeFrameX = metrics.slotShapeInset + shapeFrameX = floor((metrics.slotWidth - pillSize.width) / 2.0) } - let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: metrics.shapeSize) + let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: pillSize) let backgroundFrame: CGRect if isStretched { - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: metrics.shapeSize.height)) + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: pillSize.height)) } else { backgroundFrame = shapeFrame } @@ -244,7 +402,6 @@ private final class ItemListRevealOptionNode: ASDisplayNode { self.isExpanded = isExpanded self.didApplyLayout = true - let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude)) let contentAlpha: CGFloat if isPrimary { contentAlpha = revealProgress @@ -255,8 +412,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode { transition.updateAlpha(node: self.contentContainerNode, alpha: contentAlpha) transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale)) - let titleAlpha: CGFloat = isPrimary ? (1.0 - expandedProgress) : 1.0 - transition.updateAlpha(node: self.titleNode, alpha: titleAlpha) + let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0 + var didApplyManualIconAnimation = false let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX let iconCenterX: CGFloat @@ -281,7 +438,13 @@ private final class ItemListRevealOptionNode: ASDisplayNode { imageSize = CGSize(width: floorToScreenPixels(imageSize.width * imageScale), height: floorToScreenPixels(imageSize.height * imageScale)) } let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + 6.0 + self.animationNodeOffset), size: imageSize) - transition.updateFrame(node: animationNode, frame: iconFrame) + + if isPrimary { + didApplyManualIconAnimation = true + self.updateManualIconAnimation(iconNode: animationNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne) + } else { + transition.updateFrame(node: animationNode, frame: iconFrame) + } if self.enableAnimations { if revealProgress >= 0.4 { animationNode.play() @@ -297,11 +460,26 @@ private final class ItemListRevealOptionNode: ASDisplayNode { fittedSize = CGSize(width: floorToScreenPixels(fittedSize.width * imageScale), height: floorToScreenPixels(fittedSize.height * imageScale)) } let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize) - transition.updateFrame(node: iconNode, frame: iconFrame) + if isPrimary { + didApplyManualIconAnimation = true + self.updateManualIconAnimation(iconNode: iconNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne) + } else { + transition.updateFrame(node: iconNode, frame: iconFrame) + } } - let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX - let titleFrame = CGRect(origin: CGPoint(x: floor(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize) + if !didApplyManualIconAnimation { + self.stopManualIconAnimation() + transition.updateAlpha(node: self.titleNode, alpha: titleAlpha) + } + + let titleFrame: CGRect + if self.displaysTitleInsidePill { + titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - titleSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - titleSize.height / 2.0)), size: titleSize) + } else { + let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX + titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize) + } transition.updateFrame(node: self.titleNode, frame: titleFrame) } @@ -315,6 +493,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode { public final class ItemListRevealOptionsNode: ASDisplayNode { private let optionSelected: (ItemListRevealOption) -> Void private let tapticAction: () -> Void + private let clippingContainerNode: ASDisplayNode + private let optionsContainerNode: ASDisplayNode private var options: [ItemListRevealOption] = [] private var isLeft: Bool = false @@ -326,8 +506,14 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { public init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) { self.optionSelected = optionSelected self.tapticAction = tapticAction + self.clippingContainerNode = ASDisplayNode() + self.optionsContainerNode = ASDisplayNode() super.init() + + self.clippingContainerNode.clipsToBounds = true + self.addSubnode(self.clippingContainerNode) + self.clippingContainerNode.addSubnode(self.optionsContainerNode) } override public func didLoad() { @@ -363,11 +549,11 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { } if isLeft { for node in self.optionNodes.reversed() { - self.addSubnode(node) + self.optionsContainerNode.addSubnode(node) } } else { for node in self.optionNodes { - self.addSubnode(node) + self.optionsContainerNode.addSubnode(node) } } self.invalidateCalculatedLayout() @@ -402,6 +588,16 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { let primaryIndex = self.isLeft ? 0 : self.optionNodes.count - 1 let stride = metrics.shapeSize.width + optionSpacing + let clippingFrameX: CGFloat + if self.isLeft { + clippingFrameX = max(0.0, size.width - revealedDistance) + } else { + clippingFrameX = 0.0 + } + let clippingFrame = CGRect(origin: CGPoint(x: clippingFrameX, y: 0.0), size: CGSize(width: revealedDistance, height: size.height)) + transition.updateFrame(node: self.clippingContainerNode, frame: clippingFrame) + transition.updateFrame(node: self.optionsContainerNode, frame: CGRect(origin: CGPoint(x: -clippingFrameX, y: 0.0), size: CGSize(width: max(size.width, revealedDistance), height: size.height))) + let animated = transition.isAnimated var completionCount = self.optionNodes.count let intermediateCompletion = { @@ -419,12 +615,8 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { let isStretched = isPrimary && overswipeDistance > CGFloat.ulpOfOne let isExpanded = isPrimary && overswipeDistance > expandedActivationDistance let expandedProgress: CGFloat = isExpanded ? 1.0 : 0.0 - var nodeTransition = transition - if node.hasAppliedLayout && node.isExpanded != isExpanded { - nodeTransition = transition.isAnimated ? transition : .animated(duration: 0.2, curve: .easeInOut) - if !transition.isAnimated { - self.tapticAction() - } + if node.hasAppliedLayout && node.isExpanded != isExpanded && !transition.isAnimated { + self.tapticAction() } let baseCircleFrame: CGRect @@ -477,7 +669,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { intermediateCompletion() }) - node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: nodeTransition) + node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: transition) if self.isLeft { i -= 1 diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index fc62243b86..ab096a8865 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -74,7 +74,33 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init( + presentationData: ItemListPresentationData, + systemStyle: ItemListSystemStyle = .legacy, + icon: UIImage? = nil, + context: AccountContext? = nil, + iconPeer: EnginePeer? = nil, + title: String, + attributedTitle: NSAttributedString? = nil, + enabled: Bool = true, + titleColor: ItemListDisclosureItemTitleColor = .primary, + titleFont: ItemListDisclosureItemTitleFont = .regular, + titleIcon: UIImage? = nil, + titleBadge: String? = nil, + label: String, + attributedLabel: NSAttributedString? = nil, + labelStyle: ItemListDisclosureLabelStyle = .text, + additionalDetailLabel: String? = nil, + additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, + sectionId: ItemListSectionId, + style: ItemListStyle, + disclosureStyle: ItemListDisclosureStyle = .arrow, + noInsets: Bool = false, + action: (() -> Void)?, + clearHighlightAutomatically: Bool = true, + tag: ItemListItemTag? = nil, + shimmeringIndex: Int? = nil + ) { self.presentationData = presentationData self.systemStyle = systemStyle self.icon = icon @@ -709,7 +735,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { centralContentHeight += additionalDetailLabelInfo.0.size.height } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - centralContentHeight) / 2.0) + 1.0), size: titleLayout.size) strongSelf.titleNode.textNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { @@ -739,7 +765,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { 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) + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) } strongSelf.labelNode.frame = labelFrame diff --git a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift index 0f3cfc14d2..7e02cd7259 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift @@ -67,6 +67,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD private var enableAnimations: Bool = true private var initialRevealOffset: CGFloat = 0.0 + private var hasActiveRevealGestureOffset: Bool = false public private(set) var revealOffset: CGFloat = 0.0 private var recognizer: ItemListRevealOptionsGestureRecognizer? @@ -190,17 +191,22 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD @objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) self.revealOptionsInteractivelyClosed() } } + private func revealSpringCurve(initialVelocity: CGFloat = 0.0) -> ContainedViewLayoutTransitionCurve { + return .customSpring(mass: 2.0, stiffness: 200.0, damping: 100.0, initialVelocity: initialVelocity) + } + @objc private func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { guard let (size, _, _) = self.validLayout else { return } switch recognizer.state { case .began: + self.hasActiveRevealGestureOffset = !self.revealOffset.isZero if let leftRevealNode = self.leftRevealNode { let revealSize = leftRevealNode.bounds.size let location = recognizer.location(in: self.view) @@ -232,16 +238,26 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) { self.setupAndAddLeftRevealNode() self.revealOptionsInteractivelyOpened() + self.hasActiveRevealGestureOffset = true } else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) { self.setupAndAddRightRevealNode() self.revealOptionsInteractivelyOpened() + self.hasActiveRevealGestureOffset = true + } + if !translation.x.isZero { + self.hasActiveRevealGestureOffset = true } self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) - if self.leftRevealNode == nil && self.rightRevealNode == nil { + if self.leftRevealNode == nil && self.rightRevealNode == nil && !self.hasActiveRevealGestureOffset { self.revealOptionsInteractivelyClosed() } case .ended, .cancelled: + let hasActiveRevealGestureOffset = self.hasActiveRevealGestureOffset + self.hasActiveRevealGestureOffset = false guard let recognizer = self.recognizer else { + if hasActiveRevealGestureOffset { + self.revealOptionsInteractivelyClosed() + } break } @@ -270,7 +286,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD reveal = false selectedOption = self.revealOptions.left.first } else { - self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) } if let selectedOption = selectedOption { @@ -305,7 +321,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD reveal = false selectedOption = self.revealOptions.right.last } else { - self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) } if let selectedOption = selectedOption { @@ -315,6 +331,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD self.revealOptionsInteractivelyClosed() } } + } else if hasActiveRevealGestureOffset { + self.revealOptionsInteractivelyClosed() } default: break @@ -473,7 +491,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD } let transition: ContainedViewLayoutTransition if animated { - transition = .animated(duration: 0.3, curve: .spring) + transition = .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)) } else { transition = .immediate } @@ -495,7 +513,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD open func animateRevealOptionsFill(completion: (() -> Void)? = nil) { if let validLayout = self.validLayout { self.layer.allowsGroupOpacity = true - self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.3, curve: .spring), completion: { + self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)), completion: { self.layer.allowsGroupOpacity = false completion?() }) diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index b21392f866..c238ddb3a3 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -174,12 +174,15 @@ public class InfoItemNode: ListViewItemNode { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false + self.labelNode.displaysAsynchronously = false self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = .staticText diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index bb6552e96f..e9f93e82c2 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -171,6 +171,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { public var tag: ItemListItemTag? { return self.item?.tag } + + public var contextSourceView: UIView { + return self.textNode?.view ?? self.switchNode.view + } public init(type: ItemListSwitchItemNodeType) { self.backgroundNode = ASDisplayNode() @@ -251,6 +255,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { }) }) } + + public func updateHasContextMenu(hasContextMenu: Bool) { + let transition: ContainedViewLayoutTransition + if hasContextMenu { + transition = .immediate + } else { + transition = .animated(duration: 0.3, curve: .easeInOut) + } + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: hasContextMenu ? 0.5 : 1.0) + } else { + transition.updateAlpha(node: self.switchNode, alpha: hasContextMenu ? 0.5 : 1.0) + } + } func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -488,7 +506,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))) } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset + 1.0), size: titleLayout.size) transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin) strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index 7516769aef..302c8ceae6 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -81,6 +81,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index a0ce070a00..2a3f2b576b 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -133,6 +133,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.descriptionNode.textAlignment = .center self.peersScrollNode = ASScrollNode() self.peersScrollNode.view.showsHorizontalScrollIndicator = false + self.peersScrollNode.view.scrollsToTop = false self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0) diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift index 95fd3d3982..53382b7d12 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift @@ -76,6 +76,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, ASScro self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h index 847e2b5c7c..d0d054545a 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h @@ -14,7 +14,7 @@ @property (nonatomic, copy) void (^captionSet)(id, NSAttributedString *); @property (nonatomic, copy) void (^donePressed)(id); -@property (nonatomic, copy) void (^doneLongPressed)(id); +@property (nonatomic, copy) void (^doneLongPressed)(id, UIView *); @property (nonatomic, copy) void (^photoStripItemSelected)(NSInteger index); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 4afdca957e..1e8c90aa47 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -1,6 +1,7 @@ #import #import #import +#import @class TGPaintingData; @class TGStickerMaskDescription; @@ -142,6 +143,8 @@ @property (nonatomic, copy) id _Nullable(^ _Nullable captionPanelView)(void); @property (nonatomic, copy) id _Nullable(^ _Nullable livePhotoButton)(void); +@property (nonatomic, copy) UIView *_Nullable(^ _Nullable photoToolbarView)(TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, bool hasSendStarsButton); +@property (nonatomic, copy) bool (^ _Nullable presentMediaPickerSendActionMenu)(UIView * _Nonnull sourceView, bool canSendSilently, bool canSendWhenOnline, bool canSchedule, bool reminder, bool hasTimer, void (^ _Nonnull sendSilently)(void), void (^ _Nonnull sendWhenOnline)(void), void (^ _Nonnull schedule)(void), void (^ _Nonnull sendWithTimer)(void)); @property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull)); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index 4860d82e10..90e8982d23 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -1,44 +1,14 @@ #import +#import #import @protocol TGPhotoPaintStickersContext; -typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) { - TGPhotoEditorNoneTab = 0, - TGPhotoEditorCropTab = 1 << 0, - TGPhotoEditorRotateTab = 1 << 1, - TGPhotoEditorMirrorTab = 1 << 2, - TGPhotoEditorPaintTab = 1 << 3, - TGPhotoEditorEraserTab = 1 << 4, - TGPhotoEditorStickerTab = 1 << 5, - TGPhotoEditorTextTab = 1 << 6, - TGPhotoEditorToolsTab = 1 << 7, - TGPhotoEditorQualityTab = 1 << 8, - TGPhotoEditorTimerTab = 1 << 9, - TGPhotoEditorAspectRatioTab = 1 << 10, - TGPhotoEditorTintTab = 1 << 11, - TGPhotoEditorBlurTab = 1 << 12, - TGPhotoEditorCurvesTab = 1 << 13 -}; - -typedef enum -{ - TGPhotoEditorBackButtonBack, - TGPhotoEditorBackButtonCancel -} TGPhotoEditorBackButton; - -typedef enum -{ - TGPhotoEditorDoneButtonSend, - TGPhotoEditorDoneButtonCheck, - TGPhotoEditorDoneButtonDone, - TGPhotoEditorDoneButtonSchedule -} TGPhotoEditorDoneButton; - -@interface TGPhotoToolbarView : UIView +@interface TGPhotoToolbarView : UIView @property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; +@property (nonatomic, assign) CGFloat bottomInset; @property (nonatomic, readonly) UIButton *doneButton; @@ -79,8 +49,12 @@ typedef enum - (void)setActiveTab:(TGPhotoEditorTab)tab; +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset; +- (void)setTimerButtonValue:(NSInteger)value; + - (void)setInfoString:(NSString *)string; +- (UIView *)viewForTab:(TGPhotoEditorTab)tab; - (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h new file mode 100644 index 0000000000..043207594e --- /dev/null +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h @@ -0,0 +1,83 @@ +#import +#import + +typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) { + TGPhotoEditorNoneTab = 0, + TGPhotoEditorCropTab = 1 << 0, + TGPhotoEditorRotateTab = 1 << 1, + TGPhotoEditorMirrorTab = 1 << 2, + TGPhotoEditorPaintTab = 1 << 3, + TGPhotoEditorEraserTab = 1 << 4, + TGPhotoEditorStickerTab = 1 << 5, + TGPhotoEditorTextTab = 1 << 6, + TGPhotoEditorToolsTab = 1 << 7, + TGPhotoEditorQualityTab = 1 << 8, + TGPhotoEditorTimerTab = 1 << 9, + TGPhotoEditorAspectRatioTab = 1 << 10, + TGPhotoEditorTintTab = 1 << 11, + TGPhotoEditorBlurTab = 1 << 12, + TGPhotoEditorCurvesTab = 1 << 13 +}; + +typedef enum +{ + TGPhotoEditorBackButtonBack, + TGPhotoEditorBackButtonCancel +} TGPhotoEditorBackButton; + +typedef enum +{ + TGPhotoEditorDoneButtonSend, + TGPhotoEditorDoneButtonCheck, + TGPhotoEditorDoneButtonDone, + TGPhotoEditorDoneButtonSchedule +} TGPhotoEditorDoneButton; + +@protocol TGPhotoToolbarViewProtocol + +@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; +@property (nonatomic, assign) CGFloat bottomInset; + +@property (nonatomic, readonly) UIView *doneButton; + +@property (nonatomic, copy) void(^cancelPressed)(void); +@property (nonatomic, copy) void(^donePressed)(void); +@property (nonatomic, copy) void(^doneLongPressed)(id sender); +@property (nonatomic, copy) void(^tabPressed)(TGPhotoEditorTab tab); + +@property (nonatomic, readonly) CGRect cancelButtonFrame; +@property (nonatomic, readonly) CGRect doneButtonFrame; + +@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType; +@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType; + +@property (nonatomic, assign) int64_t sendPaidMessageStars; + +@property (nonatomic, readonly) TGPhotoEditorTab currentTabs; + +- (void)transitionInAnimated:(bool)animated; +- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent; +- (void)transitionOutAnimated:(bool)animated; +- (void)transitionOutAnimated:(bool)animated transparent:(bool)transparent hideOnCompletion:(bool)hideOnCompletion; + +- (void)setDoneButtonEnabled:(bool)enabled animated:(bool)animated; +- (void)setEditButtonsEnabled:(bool)enabled animated:(bool)animated; +- (void)setEditButtonsHidden:(bool)hidden animated:(bool)animated; +- (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; + +- (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated; +- (void)setActiveTab:(TGPhotoEditorTab)tab; + +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset; +- (void)setTimerButtonValue:(NSInteger)value; + +- (void)setInfoString:(NSString *)string; + +- (UIView *)viewForTab:(TGPhotoEditorTab)tab; + +@end diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index ee9724b20e..10f823ceda 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1548,7 +1548,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus __weak TGModernGalleryController *weakGalleryController = galleryController; __weak TGMediaPickerGalleryModel *weakModel = model; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) { + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; if (strongSelf == nil || !(strongSelf.hasSilentPosting || strongSelf.hasSchedule) || strongSelf->_shortcut) @@ -1720,6 +1720,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus [strongSelf _dismissTransitionForResultController:strongController]; }); }; + if (sourceView != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu(sourceView, strongSelf->_hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, strongSelf->_reminder, strongSelf->_hasTimer, ^{ + if (controller.sendSilently != nil) + controller.sendSilently(); + }, ^{ + if (controller.sendWhenOnline != nil) + controller.sendWhenOnline(); + }, ^{ + if (controller.schedule != nil) + controller.schedule(); + }, ^{ + if (controller.sendWithTimer != nil) + controller.sendWithTimer(); + })) { + return; + } id windowManager = nil; windowManager = [strongSelf->_context makeOverlayWindowManager]; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 28564775a5..84a2531334 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -42,6 +42,19 @@ #import #import +#import + +static UIView *TGMediaPickerCreatePhotoToolbarView(id context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id stickersContext, bool hasSendStarsButton) +{ + if (stickersContext.photoToolbarView != nil) + { + UIView *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, hasSendStarsButton); + if (toolbarView != nil) + return toolbarView; + } + + return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:hasSendStarsButton ? stickersContext : nil]; +} static TGMediaAsset *TGMediaPickerGalleryLivePhotoAsset(id editableMediaItem) { @@ -101,8 +114,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * UIView *_wrapperView; UIView *_headerWrapperView; - TGPhotoToolbarView *_portraitToolbarView; - TGPhotoToolbarView *_landscapeToolbarView; + UIView *_portraitToolbarView; + UIView *_landscapeToolbarView; UIImageView *_arrowView; UILabel *_recipientLabel; @@ -224,8 +237,10 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * return; [strongSelf.window endEditing:true]; - if (strongSelf->_doneLongPressed != nil) - strongSelf->_doneLongPressed(strongSelf->_currentItem); + if (strongSelf->_doneLongPressed != nil) { + UIView *sourceView = [sender isKindOfClass:[UIView class]] ? (UIView *)sender : nil; + strongSelf->_doneLongPressed(strongSelf->_currentItem, sourceView); + } [[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"]; }; @@ -471,13 +486,13 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil]; + _portraitToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, editingContext.sendPaidMessageStars > 0); _portraitToolbarView.cancelPressed = toolbarCancelPressed; _portraitToolbarView.donePressed = toolbarDonePressed; _portraitToolbarView.doneLongPressed = toolbarDoneLongPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil]; + _landscapeToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, false); _landscapeToolbarView.cancelPressed = toolbarCancelPressed; _landscapeToolbarView.donePressed = toolbarDonePressed; _landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed; @@ -641,17 +656,17 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * - (UIView *)timerButton { if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) - return [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + return [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab]; else - return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; + return [_landscapeToolbarView viewForTab:TGPhotoEditorTimerTab]; } - (UIView *)qualityButton { if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) - return [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; + return [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab]; else - return [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; + return [_landscapeToolbarView viewForTab:TGPhotoEditorQualityTab]; } - (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel @@ -1188,19 +1203,15 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _muteButton.selected = adjustments.sendAsGif; - TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; + UIView *qualityButton = [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab]; if (qualityButton != nil) { bool isPhoto = [_currentItemView isKindOfClass:[TGMediaPickerGalleryPhotoItemView class]] || [_currentItem isKindOfClass:[TGCameraCapturedPhoto class]]; + bool isHd = false; + TGMediaVideoConversionPreset preset = TGMediaVideoConversionPresetCompressedMedium; if (isPhoto) { - bool isHd = _editingContext.isHighQualityPhoto; - UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:isHd filled: false]; - qualityButton.iconImage = icon; - - qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; - qualityButton.iconImage = icon; + isHd = _editingContext.isHighQualityPhoto; } else { - TGMediaVideoConversionPreset preset = 0; TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault; if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]]) adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset; @@ -1221,28 +1232,19 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions]; if (preset > bestPreset) preset = bestPreset; - - UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset]; - qualityButton.iconImage = icon; - - qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; - qualityButton.iconImage = icon; } + + [_portraitToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset]; + [_landscapeToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset]; } - TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + UIView *timerButton = [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab]; if (timerButton != nil) { NSInteger value = [timer integerValue]; - UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0]; - UIImage *icon = [TGPhotoEditorInterfaceAssets timerIconForValue:value]; - [timerButton setIconImage:defaultIcon activeIconImage:icon]; - - TGPhotoEditorButton *landscapeTimerButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; - - timerButton = landscapeTimerButton; - [timerButton setIconImage:defaultIcon activeIconImage:icon]; + [_portraitToolbarView setTimerButtonValue:value]; + [_landscapeToolbarView setTimerButtonValue:value]; if (value > 0) highlightedButtons |= TGPhotoEditorTimerTab; @@ -1413,6 +1415,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _coverButton.alpha = alpha; _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; + _captionMixin.livePhotoButtonView.alpha = alpha; } completion:^(BOOL finished) { if (finished) @@ -1420,6 +1423,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; _coverButton.userInteractionEnabled = !hidden; + _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } }]; @@ -1448,6 +1452,9 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; + + _captionMixin.livePhotoButtonView.alpha = alpha; + _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } if (hidden) @@ -1735,11 +1742,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [self setSelectionInterfaceHidden:true animated:true]; - [UIView animateWithDuration:0.2 animations:^ + [UIView animateWithDuration:0.3 animations:^ { _captionMixin.inputPanelView.alpha = 0.0f; - _portraitToolbarView.doneButton.alpha = 0.0f; - _landscapeToolbarView.doneButton.alpha = 0.0f; + _portraitToolbarView.alpha = 0.0f; + _landscapeToolbarView.alpha = 0.0f; }]; } @@ -1747,11 +1754,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [self setSelectionInterfaceHidden:false animated:true]; - [UIView animateWithDuration:0.3 animations:^ + [UIView animateWithDuration:0.2 animations:^ { _captionMixin.inputPanelView.alpha = 1.0f; - _portraitToolbarView.doneButton.alpha = 1.0f; - _landscapeToolbarView.doneButton.alpha = 1.0f; + _portraitToolbarView.alpha = 1.0f; + _landscapeToolbarView.alpha = 1.0f; }]; } @@ -1861,7 +1868,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * break; default: - frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height); + frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 74.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height); break; } @@ -1972,7 +1979,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * break; default: - frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44); + frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44); break; } @@ -2085,6 +2092,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * CGFloat screenSide = MAX(screenSize.width, screenSize.height); UIEdgeInsets screenEdges = UIEdgeInsetsZero; + _portraitToolbarView.bottomInset = _safeAreaInset.bottom; + if (TGIsPad()) { _landscapeToolbarView.hidden = true; @@ -2118,7 +2127,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _coverTitleLabel.frame = CGRectMake(screenEdges.left + floor((self.frame.size.width - _coverTitleLabel.frame.size.width) / 2.0), coverTitleTopY + 26, _coverTitleLabel.frame.size.width, _coverTitleLabel.frame.size.height); UIEdgeInsets captionEdgeInsets = screenEdges; - captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height; + captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height + 10.0; [_captionMixin updateLayoutWithFrame:self.bounds edgeInsets:captionEdgeInsets animated:false]; switch (orientation) @@ -2157,14 +2166,14 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [UIView performWithoutAnimation:^ { - _photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38); + _photoCounterButton.frame = CGRectMake(screenEdges.right - 64 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38); - _selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize); + _selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 64 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize); }]; _landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height); - _headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0); + _headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 74.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0); } break; } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index 35a79b3278..12edfa97bd 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -142,7 +142,7 @@ strongSelf.completeWithItem(item, false, 0); }; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) { + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil || !(hasSilentPosting || hasSchedule)) return; @@ -236,6 +236,19 @@ strongSelf.completeWithItem(item, false, 0); }); }; + if (sourceView != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, reminder, false, ^{ + if (controller.sendSilently != nil) + controller.sendSilently(); + }, ^{ + if (controller.sendWhenOnline != nil) + controller.sendWhenOnline(); + }, ^{ + if (controller.schedule != nil) + controller.schedule(); + }, ^{ + })) { + return; + } TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[strongSelf->_context makeOverlayWindowManager] parentController:strongSelf->_parentController contentController:controller]; controllerWindow.hidden = false; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCropController.h b/submodules/LegacyComponents/Sources/TGPhotoCropController.h index 1a32504b05..6d6a873680 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCropController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoCropController.h @@ -21,6 +21,7 @@ - (void)rotate; - (void)mirror; - (void)aspectRatioButtonPressed; +- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView; - (void)setImage:(UIImage *)image; - (void)setSnapshotImage:(UIImage *)snapshotImage; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCropController.m b/submodules/LegacyComponents/Sources/TGPhotoCropController.m index f197b0418f..eb854e0fa7 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCropController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCropController.m @@ -22,8 +22,6 @@ #import "TGPhotoCropView.h" #import -#import - const CGFloat TGPhotoCropButtonsWrapperSize = 61.0f; const CGSize TGPhotoCropAreaInsetSize = { 9, 9 }; @@ -535,6 +533,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; } - (void)aspectRatioButtonPressed +{ + [self aspectRatioButtonPressedWithSourceView:nil]; +} + +- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView { if (_cropView.isAnimating) return; @@ -547,16 +550,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; { [_cropView performConfirmAnimated:true]; - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.hasSwipeGesture = true; - __weak TGMenuSheetController *weakController = controller; __weak TGPhotoCropController *weakSelf = self; void (^action)(NSString *) = ^(NSString *ratioString) { __strong TGPhotoCropController *strongSelf = weakSelf; - __strong TGMenuSheetController *strongController = weakController; if (strongSelf == nil) return; @@ -569,7 +567,7 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; else { aspectRatio = [ratioString floatValue]; - if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight) + if (strongSelf->_cropView.cropOrientation == UIImageOrientationLeft || strongSelf->_cropView.cropOrientation == UIImageOrientationRight) aspectRatio = 1.0f / aspectRatio; } @@ -588,13 +586,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; else TGDispatchAfter(0.1f, dispatch_get_main_queue(), setAspectRatioBlock); #pragma clang diagnostic pop - - [strongController dismissAnimated:true]; }; - NSMutableArray *items = [[NSMutableArray alloc] init]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(TGPhotoCropOriginalAspectRatio); }]]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(@"1.0"); }]]; + NSMutableArray *actions = [[NSMutableArray alloc] init]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") action:TGPhotoCropOriginalAspectRatio]]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") action:@"1.0"]]; CGSize croppedImageSize = _cropView.cropRect.size; if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight) @@ -634,26 +630,14 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; ratio = heightComponent / widthComponent; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] action:[NSString stringWithFormat:@"%f", ratio]]]; } - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ + [_context presentActionSheet:actions view:(sourceView ?: self.view) completion:^(LegacyComponentsActionSheetAction *selectedAction) { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }]]; - - [controller setItemViews:items]; -// controller.sourceRect = ^CGRect -// { -// __strong TGPhotoCropController *strongSelf = weakSelf; -// if (strongSelf != nil) -// return [strongSelf.view convertRect:strongSelf->_aspectRatioButton.frame fromView:strongSelf->_aspectRatioButton.superview]; -// -// return CGRectZero; -// }; - [controller presentInViewController:self.parentViewController sourceView:self.view animated:true]; + if (selectedAction != nil) + action(selectedAction.action); + }]; } [self _updateTabs]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m index c85605aee8..0fdaa96527 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m @@ -169,25 +169,7 @@ - (void)blurButtonPressed:(TGPhotoEditorBlurTypeButton *)sender { -// if (sender.tag != 0 && sender.tag == _currentType) -// { -// _editingIntensity = true; -// _startIntensity = [(PGBlurToolValue *)self.value intensity]; -// -// PGBlurToolValue *value = [(PGBlurToolValue *)self.value copy]; -// value.editingIntensity = true; -// -// _value = value; -// -// if (self.valueChanged != nil) -// self.valueChanged(value); -// -// [self setIntensitySliderHidden:false animated:true]; -// } -// else -// { - [self setSelectedBlurType:(PGBlurToolType)sender.tag update:true]; -// } + [self setSelectedBlurType:(PGBlurToolType)sender.tag update:true]; } - (void)setValue:(id)value diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index f61c9a9f19..c35be4ef10 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -27,6 +27,7 @@ #import #import +#import #import "TGPhotoEditorPreviewView.h" #import @@ -46,11 +47,21 @@ #import "TGMediaPickerGalleryVideoScrubber.h" #import "TGMediaPickerGalleryVideoScrubberThumbnailView.h" -#import - #import #import +static UIView *TGPhotoEditorCreatePhotoToolbarView(id context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id stickersContext) +{ + if (stickersContext.photoToolbarView != nil) + { + UIView *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, false); + if (toolbarView != nil) + return toolbarView; + } + + return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:nil]; +} + @interface TGPhotoEditorController () { bool _switchingTab; @@ -64,8 +75,8 @@ UIView *_containerView; UIView *_wrapperView; UIView *_transitionWrapperView; - TGPhotoToolbarView *_portraitToolbarView; - TGPhotoToolbarView *_landscapeToolbarView; + UIView *_portraitToolbarView; + UIView *_landscapeToolbarView; TGPhotoEditorPreviewView *_previewView; PGPhotoEditorView *_fullPreviewView; UIView *_fullEntitiesView; @@ -285,16 +296,27 @@ case TGPhotoEditorRotateTab: case TGPhotoEditorMirrorTab: - case TGPhotoEditorAspectRatioTab: if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]] || [strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]]) [strongSelf->_currentTabController handleTabAction:tab]; break; + + case TGPhotoEditorAspectRatioTab: + if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]]) + { + UIView *toolbarView = UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation) ? strongSelf->_portraitToolbarView : strongSelf->_landscapeToolbarView; + [(TGPhotoCropController *)strongSelf->_currentTabController aspectRatioButtonPressedWithSourceView:[toolbarView viewForTab:TGPhotoEditorAspectRatioTab]]; + } + else if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]]) + { + [strongSelf->_currentTabController handleTabAction:tab]; + } + break; } }; TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel; TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; + _portraitToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext); [_portraitToolbarView setToolbarTabs:_availableTabs animated:false]; [_portraitToolbarView setActiveTab:_currentTab]; _portraitToolbarView.cancelPressed = toolbarCancelPressed; @@ -303,7 +325,7 @@ _portraitToolbarView.tabPressed = toolbarTabPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; + _landscapeToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext); [_landscapeToolbarView setToolbarTabs:_availableTabs animated:false]; [_landscapeToolbarView setActiveTab:_currentTab]; _landscapeToolbarView.cancelPressed = toolbarCancelPressed; @@ -1974,34 +1996,12 @@ if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments])) { - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.narrowInLandscape = true; - __weak TGMenuSheetController *weakController = controller; - - NSArray *items = @ + NSArray *actions = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController == nil) - return; - - [strongController dismissAnimated:true manual:false completion:^ - { - dismiss(); - }]; - }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }] + [[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") action:@"discard" type:LegacyComponentsActionSheetActionTypeDestructive] ]; - [controller setItemViews:items]; - controller.sourceRect = ^ + [_context presentActionSheet:actions view:self.view sourceRect:^CGRect { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -2011,8 +2011,11 @@ return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView]; else return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView]; - }; - [controller presentInViewController:self sourceView:self.view animated:true]; + } completion:^(LegacyComponentsActionSheetAction *selectedAction) + { + if ([selectedAction.action isEqualToString:@"discard"]) + dismiss(); + }]; } else { diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m index 9ab2a97410..8a22a46a4c 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m @@ -30,11 +30,11 @@ + (UIColor *)accentColor { - TGMediaAssetsPallete *pallete = nil; - if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)]) - pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete]; - - return pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff); +// TGMediaAssetsPallete *pallete = nil; +// if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)]) +// pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete]; +// + return UIColorRGB(0xffd300); //pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff); } + (UIColor *)panelBackgroundColor @@ -374,7 +374,7 @@ + (UIFont *)editorItemTitleFont { - return [TGFont systemFontOfSize:14]; + return [TGFont boldSystemFontOfSize:13]; //[TGFont systemFontOfSize:1]; } + (UIColor *)filterSelectionColor diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index 8b519b5860..2c98f01c3b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -31,6 +31,7 @@ bool _animatingCancelDoneButtons; } + @end @implementation TGPhotoToolbarView @@ -635,6 +636,34 @@ return nil; } +- (UIView *)viewForTab:(TGPhotoEditorTab)tab +{ + return [self buttonForTab:tab]; +} + +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset +{ + TGPhotoEditorButton *qualityButton = [self buttonForTab:TGPhotoEditorQualityTab]; + if (qualityButton == nil) + return; + + if (isPhoto) + qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:highQuality filled:false]; + else + qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForPreset:(TGMediaVideoConversionPreset)videoPreset]; +} + +- (void)setTimerButtonValue:(NSInteger)value +{ + TGPhotoEditorButton *timerButton = [self buttonForTab:TGPhotoEditorTimerTab]; + if (timerButton == nil) + return; + + UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0]; + UIImage *activeIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:value]; + [timerButton setIconImage:defaultIcon activeIconImage:activeIcon]; +} + - (void)layoutSubviews { CGRect backgroundFrame = self.bounds; diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index c407675ed9..b894c687d6 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -236,7 +236,7 @@ [strongController dismissWhenReadyAnimated:true]; }; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGModernGalleryController *strongController = weakGalleryController; __strong TGMediaPickerGalleryModel *strongModel = weakModel; @@ -286,6 +286,19 @@ complete(silentPosting, time); }); }; + if (sourceView != nil && stickersContext.presentMediaPickerSendActionMenu != nil && stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, hasSchedule, hasSchedule, reminder, false, ^{ + if (sendController.sendSilently != nil) + sendController.sendSilently(); + }, ^{ + if (sendController.sendWhenOnline != nil) + sendController.sendWhenOnline(); + }, ^{ + if (sendController.schedule != nil) + sendController.schedule(); + }, ^{ + })) { + return; + } [strongController presentViewController:sendController animated:false completion:nil]; }; diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index 7cdc998863..951f5a14ff 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyUI:LegacyUI", + "//submodules/ContextUI:ContextUI", "//submodules/MimeTypes:MimeTypes", "//submodules/LocalMediaResources:LocalMediaResources", "//submodules/SearchPeerMembers:SearchPeerMembers", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index c3e477107c..87c5001af8 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -166,6 +166,7 @@ public func legacyMediaEditor( snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil, hasSilentPosting: Bool = false, hasSchedule: Bool = false, reminder: Bool = false, @@ -191,6 +192,7 @@ public func legacyMediaEditor( paintStickersContext.captionPanelView = { return getCaptionPanelView() } + paintStickersContext.photoToolbarView = photoToolbarView let presentationData = context.sharedContext.currentPresentationData.with { $0 } let recipientName: String @@ -208,15 +210,28 @@ public func legacyMediaEditor( legacyController.blocksBackgroundWhenInOverlay = true legacyController.acceptsFocusWhenInOverlay = true legacyController.statusBar.statusBarStyle = .Ignore + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) legacyController.controllerLoaded = { [weak legacyController] in legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } - + legacyController.presentationCompleted = { + Queue.mainQueue().after(0.1) { + transitionCompletion?() + } + } + let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in presentSchedulePicker(media, done) } let appeared: () -> Void = { - transitionCompletion?() } let completion: (TGMediaEditableItem, TGMediaEditingContext, Bool, Int32) -> Void = { result, editingContext, silentPosting, scheduleTime in let nativeGenerator = legacyAssetPickerItemGenerator() @@ -235,64 +250,30 @@ public func legacyMediaEditor( legacyController.enableSizeClassSignal = true - if isGif { - let galleryController = TGPhotoVideoEditor.controller( - with: legacyController.context, - caption: initialCaption, - withItem: item, - paint: mode == .draw, - adjustments: mode == .adjustments, - recipientName: recipientName, - stickersContext: paintStickersContext, - from: .zero, - mainSnapshot: nil, - snapshots: snapshots as [Any], - immediate: transitionCompletion != nil, - activateInput: mode == .caption, - isGif: true, - hasSilentPosting: hasSilentPosting, - hasSchedule: hasSchedule, - reminder: reminder, - presentSchedulePicker: schedulePicker, - appeared: appeared, - completion: completion, - dismissed: dismissed - ) - legacyController.bind(controller: galleryController) - present(legacyController, nil) - } else { - let emptyController = LegacyEmptyController(context: legacyController.context)! - emptyController.navigationBarShouldBeHidden = true - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - legacyController.bind(controller: navigationController) - - present(legacyController, nil) - - TGPhotoVideoEditor.present( - with: legacyController.context, - controller: emptyController, - caption: initialCaption, - withItem: item, - paint: mode == .draw, - adjustments: mode == .adjustments, - recipientName: recipientName, - stickersContext: paintStickersContext, - from: .zero, - mainSnapshot: nil, - snapshots: snapshots as [Any], - immediate: transitionCompletion != nil, - activateInput: mode == .caption, - isGif: false, - hasSilentPosting: hasSilentPosting, - hasSchedule: hasSchedule, - reminder: reminder, - presentSchedulePicker: schedulePicker, - appeared: appeared, - completion: completion, - dismissed: dismissed - ) - } + let galleryController = TGPhotoVideoEditor.controller( + with: legacyController.context, + caption: initialCaption, + withItem: item, + paint: mode == .draw, + adjustments: mode == .adjustments, + recipientName: recipientName, + stickersContext: paintStickersContext, + from: .zero, + mainSnapshot: nil, + snapshots: snapshots as [Any], + immediate: transitionCompletion != nil, + activateInput: mode == .caption, + isGif: isGif, + hasSilentPosting: hasSilentPosting, + hasSchedule: hasSchedule, + reminder: reminder, + presentSchedulePicker: schedulePicker, + appeared: appeared, + completion: completion, + dismissed: dismissed + ) + legacyController.bind(controller: galleryController) + present(legacyController, nil) }) } @@ -389,6 +370,15 @@ public func legacyAttachmentMenu( } let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: updatedPresentationData.initial, presentInGlobalOverlay: { [weak parentController] controller in + if let parentController { + parentController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index b11536ead8..4d452670b9 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -21,6 +21,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: EngineRawPeer, 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, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context) paintStickersContext.captionPanelView = { return getCaptionPanelView() } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index f10578a5ff..6ded61a274 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -13,6 +13,7 @@ import SolidRoundedButtonNode import MediaEditor import DrawingUI import TelegramPresentationData +import ContextUI import AnimatedCountLabelNode import CoreMedia @@ -571,15 +572,118 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } } +public typealias LegacyMediaPickerSendActionMenuPresenter = ( + UIView, + Bool, + Bool, + Bool, + Bool, + Bool, + @escaping () -> Void, + @escaping () -> Void, + @escaping () -> Void, + @escaping () -> Void +) -> Bool + +private final class LegacyMediaPickerSendActionMenuReferenceContentSource: ContextReferenceContentSource { + private weak var sourceView: UIView? + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + guard let sourceView = self.sourceView else { + return nil + } + return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +public func makeLegacyMediaPickerSendActionMenuPresenter( + context: AccountContext, + presentationData: PresentationData? = nil, + presentInGlobalOverlay: ((ViewController) -> Void)? = nil +) -> LegacyMediaPickerSendActionMenuPresenter { + return { sourceView, canSendSilently, canSendWhenOnline, canSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer in + guard sourceView.window != nil else { + return false + } + + let presentationData = (presentationData ?? context.sharedContext.currentPresentationData.with { $0 }).withUpdated(theme: defaultDarkPresentationTheme) + var items: [ContextMenuItem] = [] + + if canSendSilently { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendSilently() + }))) + } + + if canSendWhenOnline { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendWhenOnline() + }))) + } + + if canSchedule { + items.append(.action(ContextMenuActionItem(text: reminder ? presentationData.strings.Conversation_SendMessage_SetReminder : presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + schedule() + }))) + } + + if hasTimer { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Timer_Send, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendWithTimer() + }))) + } + + guard !items.isEmpty else { + return false + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(LegacyMediaPickerSendActionMenuReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + if let presentInGlobalOverlay { + presentInGlobalOverlay(contextController) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(contextController) + } else { + context.sharedContext.presentGlobalController(contextController, nil) + } + return true + } +} + public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext { public var captionPanelView: (() -> TGCaptionPanelView?)? public var livePhotoButton: (() -> TGLivePhotoButton?)? + public var photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? + public var presentMediaPickerSendActionMenu: LegacyMediaPickerSendActionMenuPresenter? public var editCover: ((CGSize, @escaping (UIImage) -> Void) -> Void)? - + private let context: AccountContext - + public init(context: AccountContext) { self.context = context + super.init() + + self.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context) } class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter { diff --git a/submodules/LegacyUI/BUILD b/submodules/LegacyUI/BUILD index 958e606004..9fd41d27b1 100644 --- a/submodules/LegacyUI/BUILD +++ b/submodules/LegacyUI/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramCore:TelegramCore", "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/ContextUI:ContextUI", "//submodules/Display:Display", "//submodules/AccountContext:AccountContext", "//submodules/TelegramAudio:TelegramAudio", diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 0259dd86b6..7b276de96e 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import ContextUI import Display import SSignalKit import SwiftSignalKit @@ -28,6 +29,18 @@ private func passControllerAppearanceAnimated(in: Bool, presentation: LegacyCont } } +private final class LegacyActionSheetContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} + private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyComponentsOverlayWindowManager { private weak var contentController: UIViewController? private weak var parentController: ViewController? @@ -260,11 +273,80 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { } public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { - + self.presentActionSheet(actions, view: view, sourceRect: nil, completion: completion) } public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, sourceRect: (() -> CGRect)!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { + guard let controller = self.controller, let view = view else { + completion?(nil) + return + } + let presentationData: PresentationData + if let context = legacyContextGet() { + presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + } else { + presentationData = defaultPresentationData().withUpdated(theme: defaultDarkColorPresentationTheme) + } + + let anchorView: UIView? + let referenceView: UIView + if let sourceRect = sourceRect { + let anchor = UIView(frame: sourceRect()) + anchor.isUserInteractionEnabled = false + anchor.backgroundColor = .clear + view.addSubview(anchor) + anchorView = anchor + referenceView = anchor + } else { + anchorView = nil + referenceView = view + } + + var didSelectAction = false + var items: [ContextMenuItem] = [] + for legacyAction in actions ?? [] { + if legacyAction.type == LegacyComponentsActionSheetActionTypeCancel { + continue + } + guard let title = legacyAction.title else { + continue + } + let textColor: ContextMenuActionItemTextColor = legacyAction.type == LegacyComponentsActionSheetActionTypeDestructive ? .destructive : .primary + items.append(.action(ContextMenuActionItem(text: title, textColor: textColor, icon: { _ in + return nil + }, action: { actionContext in + didSelectAction = true + if let contextController = actionContext.controller { + contextController.dismiss(result: .default, completion: { + completion?(legacyAction) + }) + } else { + anchorView?.removeFromSuperview() + completion?(legacyAction) + } + }))) + } + + if items.isEmpty { + anchorView?.removeFromSuperview() + completion?(nil) + return + } + + let contextController = makeContextController( + context: legacyContextGet(), + presentationData: presentationData, + source: .reference(LegacyActionSheetContextReferenceContentSource(sourceView: referenceView)), + items: .single(ContextController.Items(content: .list(items))) + ) + contextController.dismissed = { [weak anchorView] in + anchorView?.removeFromSuperview() + if !didSelectAction { + completion?(nil) + } + } + controller.present(contextController, in: .window(.root)) } public func presentTooltip(_ text: String!, icon: UIImage!, sourceRect: CGRect) { diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index c8d8e43912..0a5f677cf3 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -4,6 +4,10 @@ import AsyncDisplayKit import Display import TelegramPresentationData +private let compactInfinityFont = Font.with(size: 14.0, design: .round, weight: .bold) +private let compactTextFont = Font.with(size: 12.0, design: .round, weight: .bold) +private let compactSmallTextFont = Font.with(size: 10.0, design: .round, weight: .bold) + private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold) private let textFont = Font.with(size: 13.0, design: .round, weight: .bold) private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold) @@ -134,11 +138,11 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { let font: UIFont if parameters.string == "∞" { - font = infinityFont + font = bounds.width < 28.0 ? compactInfinityFont : infinityFont } else if parameters.string.count > 2 { - font = smallTextFont + font = bounds.width < 28.0 ? compactSmallTextFont : smallTextFont } else { - font = textFont + font = bounds.width < 28.0 ? compactTextFont : textFont } let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor] @@ -147,7 +151,7 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { var offset = CGPoint() if parameters.string == "∞" { - offset = CGPoint(x: 1.0, y: -1.0) + offset = bounds.width < 28.0 ? CGPoint(x: 1.0 - UIScreenPixel, y: 0.0) : CGPoint(x: 1.0, y: -1.0) } else if parameters.string.count > 2 { offset = CGPoint(x: 0.0, y: UIScreenPixel) } diff --git a/submodules/LocationUI/BUILD b/submodules/LocationUI/BUILD index 321fd7dd3a..39d33fd6d0 100644 --- a/submodules/LocationUI/BUILD +++ b/submodules/LocationUI/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LegacyComponents:LegacyComponents", + "//submodules/ContextUI:ContextUI", "//submodules/ShareController:ShareController", "//submodules/AccountContext:AccountContext", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", @@ -41,14 +42,20 @@ swift_library( "//submodules/TelegramNotices:TelegramNotices", "//submodules/TooltipUI:TooltipUI", "//submodules/UndoUI:UndoUI", + "//submodules/Weather", "//submodules/AttachmentUI:AttachmentUI", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/ComponentFlow", "//submodules/Components/BundleIconComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/ViewControllerComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/SearchInputPanelComponent", "//submodules/TelegramUI/Components/ButtonComponent", diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index ea37651e03..a4c5121a60 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -141,16 +141,18 @@ final class LocationActionListItem: ListViewItem { let title: String let subtitle: String let icon: LocationActionListItemIcon + let isOpaque: Bool let beginTimeAndTimeout: (Double, Double)? let action: () -> Void let highlighted: (Bool) -> Void - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) { + public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, isOpaque: Bool = true, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) { self.presentationData = presentationData self.engine = engine self.title = title self.subtitle = subtitle self.icon = icon + self.isOpaque = isOpaque self.beginTimeAndTimeout = beginTimeAndTimeout self.action = action self.highlighted = highlighted @@ -216,6 +218,7 @@ final class LocationActionListItemNode: ListViewItemNode { self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.clipsToBounds = true self.highlightedBackgroundNode.isLayerBacked = true self.iconNode = ASImageNode() @@ -232,6 +235,21 @@ final class LocationActionListItemNode: ListViewItemNode { self.addSubnode(self.venueIconNode) } + func liveLocationContextSourceView(extend: Bool) -> UIView? { + guard let icon = self.item?.icon else { + return nil + } + + switch icon { + case .liveLocation: + return extend ? nil : self.view + case .extendLiveLocation: + return extend ? self.view : nil + default: + return nil + } + } + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() @@ -278,7 +296,7 @@ final class LocationActionListItemNode: ListViewItemNode { let iconLayout = self.venueIconNode.asyncLayout() return { [weak self] item, params, hasSeparator in - let leftInset: CGFloat = 65.0 + params.leftInset + let leftInset: CGFloat = (item.isOpaque ? 65.0 : 72.0 ) + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 8.0 let iconSize: CGFloat = 40.0 @@ -292,7 +310,7 @@ final class LocationActionListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 + let titleSpacing: CGFloat = 0.0 let bottomInset: CGFloat = hasSeparator ? 0.0 : 4.0 var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset) if hasSeparator { @@ -300,6 +318,8 @@ final class LocationActionListItemNode: ListViewItemNode { } let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + var hasSeparator = hasSeparator + return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -315,11 +335,15 @@ final class LocationActionListItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params - + if let _ = updatedTheme { strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + if item.isOpaque { + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor + } } var arguments: TransformImageCustomArguments? @@ -394,15 +418,34 @@ final class LocationActionListItemNode: ListViewItemNode { let topHighlightInset: CGFloat = separatorHeight let separatorRightInset: CGFloat = 16.0 - let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + let contentLeftInset: CGFloat = item.isOpaque ? 0.0 : 7.0 + + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) strongSelf.iconNode.frame = iconNodeFrame strongSelf.venueIconNode.frame = iconNodeFrame - strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0)) + strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0)) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) + + let highlightFrame: CGRect + let highlightCornerRadius: CGFloat + if item.isOpaque { + highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) + highlightCornerRadius = 0.0 + } else { + highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0)) + highlightCornerRadius = highlightFrame.height * 0.5 + } + + strongSelf.highlightedBackgroundNode.frame = highlightFrame + strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight)) + + if !item.isOpaque { + hasSeparator = false + } + strongSelf.backgroundNode.isHidden = !item.isOpaque strongSelf.separatorNode.isHidden = !hasSeparator if let (beginTimestamp, timeout) = item.beginTimeAndTimeout { @@ -414,9 +457,9 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.addSubnode(timerNode) strongSelf.timerNode = timerNode } - let timerSize = CGSize(width: 28.0, height: 28.0) + let timerSize = CGSize(width: 24.0, height: 24.0) timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings) - timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) + timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 15.0 - contentLeftInset - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift index a9957db257..207e31cbc6 100644 --- a/submodules/LocationUI/Sources/LocationAnnotation.swift +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -177,6 +177,7 @@ public class LocationPinAnnotationView: MKAnnotationView { var hasPulse = false var headingKvoToken: NSKeyValueObservation? + private var mapHeading: CGFloat = 0.0 override public class var layerClass: AnyClass { return LocationPinAnnotationLayer.self @@ -291,12 +292,10 @@ public class LocationPinAnnotationView: MKAnnotationView { headingKvoToken.invalidate() } - self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in - guard let heading = change.newValue else { - return - } - self?.updateHeading(heading) + self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, _) in + self?.updateHeading() } + self.updateHeading() } else if let peer = annotation.peer { self.iconNode.isHidden = true @@ -310,7 +309,7 @@ public class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken = nil headingKvoToken.invalidate() } - self.updateHeading(nil) + self.updateHeading() } else if let location = annotation.location { let venueType = location.venue?.type ?? "" let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType) @@ -347,16 +346,23 @@ public class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken = nil headingKvoToken.invalidate() } - self.updateHeading(nil) + self.updateHeading() } } } } - private func updateHeading(_ heading: NSNumber?) { - if let heading = heading?.int32Value { + func updateMapHeading(_ mapHeading: CGFloat) { + if self.mapHeading != mapHeading { + self.mapHeading = mapHeading + self.updateHeading() + } + } + + private func updateHeading() { + if let heading = (self.annotation as? LocationPinAnnotation)?.heading?.doubleValue { self.arrowNode.isHidden = false - self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0) + self.arrowNode.transform = CATransform3DMakeRotation((CGFloat(heading) - self.mapHeading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0) } else { self.arrowNode.isHidden = true self.arrowNode.transform = CATransform3DIdentity diff --git a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift index 3547383093..8e56deae75 100644 --- a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift +++ b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift @@ -1,123 +1,84 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import TelegramStringFormatting import PresentationDataUtils -import CoreLocation +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import ButtonComponent +import GlassBarButtonComponent +import AnimatedTextComponent +import BundleIconComponent enum LocationDistancePickerScreenStyle { case `default` case media } -final class LocationDistancePickerScreen: ViewController { - private var controllerNode: LocationDistancePickerScreenNode { - return self.displayNode as! LocationDistancePickerScreenNode - } - - private var animatedIn = false - - private let context: AccountContext - private let style: LocationDistancePickerScreenStyle - private let distances: Signal<[Double], NoError> - private let compactDisplayTitle: String? - private let updated: (Int32?) -> Void - private let completion: (Int32, @escaping () -> Void) -> Void - private let willDismiss: () -> Void - - private var presentationDataDisposable: Disposable? +final class LocationDistancePickerScreen: ViewControllerComponentContainer { + private let willDismissImpl: () -> Void + private var didCallWillDismiss = false init(context: AccountContext, style: LocationDistancePickerScreenStyle, compactDisplayTitle: String?, distances: Signal<[Double], NoError>, updated: @escaping (Int32?) -> Void, completion: @escaping (Int32, @escaping () -> Void) -> Void, willDismiss: @escaping () -> Void) { - self.context = context - self.style = style - self.distances = distances - self.compactDisplayTitle = compactDisplayTitle - self.updated = updated - self.completion = completion - self.willDismiss = willDismiss + self.willDismissImpl = willDismiss - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore + super.init( + context: context, + component: LocationDistancePickerScreenComponent( + context: context, + style: style, + compactDisplayTitle: compactDisplayTitle, + distances: distances, + updated: { distance in + updated(distance) + }, + completion: completion, + willDismiss: willDismiss + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore + ) self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.controllerNode.updatePresentationData(presentationData) - } - }) - - self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = LocationDistancePickerScreenNode(context: self.context, style: self.style, compactDisplayTitle: self.compactDisplayTitle, distances: self.distances) - self.controllerNode.updated = { [weak self] distance in - guard let strongSelf = self else { - return - } - strongSelf.updated(distance) - } - self.controllerNode.completion = { [weak self] distance in - guard let strongSelf = self else { - return - } - strongSelf.completion(distance, { - strongSelf.dismiss() - }) - } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - self.controllerNode.cancel = { [weak self] in - self?.dismiss() - } - - let _ = self.controllerNode.update() - } - - override public func loadView() { - super.loadView() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() + fileprivate func performWillDismissOnce() { + if self.didCallWillDismiss { + return } + self.didCallWillDismiss = true + self.willDismissImpl() } override public func dismiss(completion: (() -> Void)? = nil) { - self.willDismiss() - self.controllerNode.animateOut(completion: completion) + if let componentView = self.node.hostView.componentView as? LocationDistancePickerScreenComponent.View { + componentView.requestDismiss(completion: completion) + } else { + self.performWillDismissOnce() + super.dismiss(animated: false, completion: completion) + } } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if flag { + self.dismiss(completion: completion) + } else { + super.dismiss(animated: false, completion: completion) + } } } -private class TimerPickerView: UIPickerView { +private final class TimerPickerView: UIPickerView { var selectorColor: UIColor? = nil { didSet { for subview in self.subviews { @@ -151,7 +112,7 @@ private class TimerPickerView: UIPickerView { } } -private var unitValues: [Int32] = { +private let unitValues: [Int32] = { var values: [Int32] = [] for i in 0 ..< 99 { values.append(Int32(i)) @@ -159,7 +120,7 @@ private var unitValues: [Int32] = { return values }() -private var smallUnitValues: [Int32] = { +private let smallUnitValues: [Int32] = { var values: [Int32] = [] values.append(0) values.append(5) @@ -169,213 +130,318 @@ private var smallUnitValues: [Int32] = { return values }() -class LocationDistancePickerScreenNode: ViewControllerTracingNode, ASScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate { - private let context: AccountContext - private let controllerStyle: LocationDistancePickerScreenStyle - private var presentationData: PresentationData - private var compactDisplayTitle: String? - private var distances: [Double] = [] +private final class LocationDistancePickerScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButton: HighlightableButtonNode - private let doneButton: SolidRoundedButtonNode + let context: AccountContext + let style: LocationDistancePickerScreenStyle + let compactDisplayTitle: String? + let distances: Signal<[Double], NoError> + let updated: (Int32) -> Void + let completion: (Int32, @escaping () -> Void) -> Void + let willDismiss: () -> Void - private let measureButtonTitleNode: ImmediateTextNode - - private var pickerView: TimerPickerView? - private let unitLabelNode: ImmediateTextNode - private let smallUnitLabelNode: ImmediateTextNode - - private var pickerTimer: SwiftSignalKit.Timer? - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - private var distancesDisposable: Disposable? - - var updated: ((Int32) -> Void)? - var completion: ((Int32) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, style: LocationDistancePickerScreenStyle, compactDisplayTitle: String?, distances: Signal<[Double], NoError>) { + init( + context: AccountContext, + style: LocationDistancePickerScreenStyle, + compactDisplayTitle: String?, + distances: Signal<[Double], NoError>, + updated: @escaping (Int32) -> Void, + completion: @escaping (Int32, @escaping () -> Void) -> Void, + willDismiss: @escaping () -> Void + ) { self.context = context - self.controllerStyle = style - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.style = style self.compactDisplayTitle = compactDisplayTitle - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor: UIColor - let textColor: UIColor - let accentColor: UIColor - let blurStyle: UIBlurEffect.Style - switch style { - case .default: - backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - textColor = self.presentationData.theme.actionSheet.primaryTextColor - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - case .media: - backgroundColor = UIColor(rgb: 0x1c1c1e) - textColor = .white - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = .dark - } - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Location_ProximityNotification_Title, font: Font.bold(17.0), textColor: textColor) - - self.textNode = ImmediateTextNode() - self.textNode.alpha = 0.0 - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal) - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0) - self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send - - self.unitLabelNode = ImmediateTextNode() - self.smallUnitLabelNode = ImmediateTextNode() - - self.measureButtonTitleNode = ImmediateTextNode() - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: textColor) - self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: textColor) - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - self.contentContainerNode.addSubnode(self.cancelButton) - self.contentContainerNode.addSubnode(self.doneButton) - - self.contentContainerNode.addSubnode(self.unitLabelNode) - self.contentContainerNode.addSubnode(self.smallUnitLabelNode) - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self, let pickerView = strongSelf.pickerView { - strongSelf.doneButton.isUserInteractionEnabled = false - - let largeValue = unitValues[pickerView.selectedRow(inComponent: 0)] - let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)] - var value = largeValue * 1000 + smallValue * 10 - if !strongSelf.usesMetricSystem { - value = Int32(Double(value) * 1.60934) - } - strongSelf.completion?(value) - } - } - - self.setupPickerView() - - self.distancesDisposable = (distances - |> deliverOnMainQueue).start(next: { [weak self] distances in - if let strongSelf = self { - strongSelf.distances = distances - strongSelf.updateDoneButtonTitle() - } - }) + self.distances = distances + self.updated = updated + self.completion = completion + self.willDismiss = willDismiss } - deinit { - self.distancesDisposable?.dispose() - - self.pickerTimer?.invalidate() - } - - func setupPickerView() { - if let pickerView = self.pickerView { - pickerView.removeFromSuperview() - } - - let pickerView = TimerPickerView() - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) - pickerView.dataSource = self - pickerView.delegate = self - pickerView.selectRow(0, inComponent: 0, animated: false) - - if self.usesMetricSystem { - pickerView.selectRow(6, inComponent: 1, animated: false) - } else { - pickerView.selectRow(4, inComponent: 1, animated: false) - } - self.contentContainerNode.view.addSubview(pickerView) - self.pickerView = pickerView - - self.contentContainerNode.addSubnode(self.unitLabelNode) - self.contentContainerNode.addSubnode(self.smallUnitLabelNode) - - self.pickerTimer?.invalidate() - - let pickerTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: true, completion: { [weak self] in - if let strongSelf = self { - if strongSelf.update() { - strongSelf.updateDoneButtonTitle() - } - } - }, queue: Queue.mainQueue()) - self.pickerTimer = pickerTimer - pickerTimer.start() - - self.updateDoneButtonTitle() - } - - private var usesMetricSystem: Bool { - let locale = localeWithStrings(self.presentationData.strings) - if locale.identifier.hasSuffix("GB") { + static func ==(lhs: LocationDistancePickerScreenComponent, rhs: LocationDistancePickerScreenComponent) -> Bool { + if lhs.context !== rhs.context { return false } - return locale.usesMetricSystem + if lhs.style != rhs.style { + return false + } + if lhs.compactDisplayTitle != rhs.compactDisplayTitle { + return false + } + return true } - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 2 + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: LocationDistancePickerScreenComponent? + private var environment: ViewControllerComponentContainer.Environment? + private var isDismissed = false + private var didCallWillDismiss = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func performWillDismissOnce() { + if self.didCallWillDismiss { + return + } + self.didCallWillDismiss = true + + if let controller = self.environment?.controller() as? LocationDistancePickerScreen { + controller.performWillDismissOnce() + } else { + self.component?.willDismiss() + } + } + + func requestDismiss(completion: (() -> Void)? = nil) { + self.performWillDismissOnce() + + if self.isDismissed { + completion?() + return + } + self.isDismissed = true + + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self else { + completion?() + return + } + if let controller = self.environment?.controller() { + controller.dismiss(animated: false, completion: completion) + } else { + completion?() + } + }) + } + + func update(component: LocationDistancePickerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + self?.requestDismiss() + } + ) + + let backgroundColor: SheetComponent.BackgroundColor + switch component.style { + case .default: + backgroundColor = .color(environment.theme.list.modalPlainBackgroundColor) + case .media: + backgroundColor = .color(UIColor(rgb: 0x1c1c1e)) + } + + let _ = self.sheet.update( + transition: transition, + component: AnyComponent( + SheetComponent( + content: AnyComponent( + LocationDistancePickerContentComponent( + style: component.style, + compactDisplayTitle: component.compactDisplayTitle, + distances: component.distances, + updated: component.updated, + completion: { [weak self] distance in + guard let self, let component = self.component else { + return + } + component.completion(distance, { [weak self] in + self?.requestDismiss() + }) + }, + dismiss: { [weak self] in + self?.requestDismiss() + } + ) + ), + style: .glass, + backgroundColor: backgroundColor, + hasDimView: false, + animateOut: self.sheetAnimateOut, + willDismiss: { [weak self] in + self?.performWillDismissOnce() + } + ) + ), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } } - private func updateDoneButtonTitle() { - if let pickerView = self.pickerView { + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class LocationDistancePickerContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let style: LocationDistancePickerScreenStyle + let compactDisplayTitle: String? + let distances: Signal<[Double], NoError> + let updated: (Int32) -> Void + let completion: (Int32) -> Void + let dismiss: () -> Void + + init( + style: LocationDistancePickerScreenStyle, + compactDisplayTitle: String?, + distances: Signal<[Double], NoError>, + updated: @escaping (Int32) -> Void, + completion: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { + self.style = style + self.compactDisplayTitle = compactDisplayTitle + self.distances = distances + self.updated = updated + self.completion = completion + self.dismiss = dismiss + } + + static func ==(lhs: LocationDistancePickerContentComponent, rhs: LocationDistancePickerContentComponent) -> Bool { + if lhs.style != rhs.style { + return false + } + if lhs.compactDisplayTitle != rhs.compactDisplayTitle { + return false + } + return true + } + + final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { + private let closeButton = ComponentView() + private let title = ComponentView() + private let unitLabel = ComponentView() + private let smallUnitLabel = ComponentView() + private let button = ComponentView() + private let warningText = ComponentView() + + private var pickerView: TimerPickerView? + private var pickerTimer: SwiftSignalKit.Timer? + private var distancesDisposable: Disposable? + + private var component: LocationDistancePickerContentComponent? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? + private var distances: [Double] = [] + private var previousReportedValue: Int32? + private var isCompleting = false + + private var isUpdating = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.distancesDisposable?.dispose() + self.pickerTimer?.invalidate() + } + + private var usesMetricSystem: Bool { + guard let environment = self.environment else { + return true + } + let locale = localeWithStrings(environment.strings) + if locale.identifier.hasSuffix("GB") { + return false + } + return locale.usesMetricSystem + } + + private func setupDistancesIfNeeded(component: LocationDistancePickerContentComponent) { + if self.distancesDisposable != nil { + return + } + self.distancesDisposable = (component.distances + |> deliverOnMainQueue).start(next: { [weak self] distances in + guard let self else { + return + } + self.distances = distances + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + private func setupPickerViewIfNeeded() { + if self.pickerView != nil { + return + } + + let pickerView = TimerPickerView() + pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) + pickerView.dataSource = self + pickerView.delegate = self + pickerView.selectRow(0, inComponent: 0, animated: false) + if self.usesMetricSystem { + pickerView.selectRow(6, inComponent: 1, animated: false) + } else { + pickerView.selectRow(4, inComponent: 1, animated: false) + } + self.addSubview(pickerView) + self.pickerView = pickerView + + let pickerTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: true, completion: { [weak self] in + guard let self else { + return + } + if self.reportSelectedValue() { + self.state?.updated(transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.pickerTimer = pickerTimer + pickerTimer.start() + + let _ = self.reportSelectedValue() + } + + private func selectedDistance() -> (value: Int32, convertedValue: Int32, convertedDistance: Double, distanceText: String)? { + guard let pickerView = self.pickerView, let environment = self.environment else { + return nil + } + let selectedLargeRow = pickerView.selectedRow(inComponent: 0) var selectedSmallRow = pickerView.selectedRow(inComponent: 1) if selectedLargeRow == 0 && selectedSmallRow == 0 { @@ -390,275 +456,309 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, ASScrollViewD if smallValue == 5 { formattedValue = formattedValue.replacingOccurrences(of: ".1", with: ".05").replacingOccurrences(of: ",1", with: ",05") } - let distance = self.usesMetricSystem ? "\(formattedValue) \(self.presentationData.strings.Location_ProximityNotification_DistanceKM)" : "\(formattedValue) \(self.presentationData.strings.Location_ProximityNotification_DistanceMI)" + let distanceText = self.usesMetricSystem ? "\(formattedValue) \(environment.strings.Location_ProximityNotification_DistanceKM)" : "\(formattedValue) \(environment.strings.Location_ProximityNotification_DistanceMI)" - let shortTitle = self.presentationData.strings.Location_ProximityNotification_Notify(distance).string - var longTitle: String? - if let displayTitle = self.compactDisplayTitle, let (layout, _) = self.containerLayout { - let title = self.presentationData.strings.Location_ProximityNotification_NotifyLong(displayTitle, distance).string - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - self.measureButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .black) - let titleSize = self.measureButtonTitleNode.updateLayout(CGSize(width: width * 2.0, height: 50.0)) - if titleSize.width < width - 70.0 { - longTitle = title + var convertedDistance = Double(value) + if !self.usesMetricSystem { + convertedDistance = convertedDistance * 1.60934 + } + + return (value, Int32(convertedDistance), convertedDistance, distanceText) + } + + private func reportSelectedValue() -> Bool { + guard let selectedDistance = self.selectedDistance(), let component = self.component else { + return false + } + if let previousReportedValue = self.previousReportedValue, selectedDistance.convertedValue == previousReportedValue { + return false + } + self.previousReportedValue = selectedDistance.convertedValue + component.updated(selectedDistance.convertedValue) + return true + } + + func update(component: LocationDistancePickerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousEnvironment = self.environment + self.component = component + self.state = state + + let environment = environment[EnvironmentType.self].value + self.environment = environment + + self.setupDistancesIfNeeded(component: component) + self.setupPickerViewIfNeeded() + + if let previousEnvironment, previousEnvironment.strings !== environment.strings || previousEnvironment.theme !== environment.theme { + self.pickerView?.reloadAllComponents() + } + + let textColor: UIColor + let secondaryTextColor: UIColor + let buttonFillColor: UIColor + let buttonForegroundColor: UIColor + switch component.style { + case .default: + textColor = environment.theme.actionSheet.primaryTextColor + secondaryTextColor = environment.theme.actionSheet.secondaryTextColor + buttonFillColor = environment.theme.list.itemCheckColors.fillColor + buttonForegroundColor = environment.theme.list.itemCheckColors.foregroundColor + case .media: + textColor = .white + secondaryTextColor = UIColor(white: 1.0, alpha: 0.7) + buttonFillColor = environment.theme.list.itemCheckColors.fillColor + buttonForegroundColor = environment.theme.list.itemCheckColors.foregroundColor + } + + let sideInset: CGFloat = 16.0 + let topInset: CGFloat = 16.0 + let titleHeight: CGFloat = 54.0 + let pickerHeight: CGFloat = 216.0 + let buttonHeight: CGFloat = 52.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) + let buttonWidth = availableSize.width - buttonInsets.left - buttonInsets.right + + let selectedDistance = self.selectedDistance() + let distanceText = selectedDistance?.distanceText ?? "" + let isTooFar: Bool + if let selectedDistance, let maximumDistance = self.distances.last { + isTooFar = selectedDistance.convertedDistance > maximumDistance + } else { + isTooFar = false + } + + let closeButtonSize = CGSize(width: 44.0, height: 44.0) + let closeSize = self.closeButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: closeButtonSize, + backgroundColor: nil, + isDark: component.style == .media ? true : environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.component?.dismiss() + } + ) + ), + environment: {}, + containerSize: closeButtonSize + ) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: closeSize)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: environment.strings.Location_ProximityNotification_Title, + font: Font.bold(17.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 120.0, height: titleHeight) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: topInset + floorToScreenPixels((closeButtonSize.height - titleSize.height) * 0.5)), size: titleSize)) + } + + let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + titleHeight), size: CGSize(width: availableSize.width, height: pickerHeight)) + if let pickerView = self.pickerView { + transition.setFrame(view: pickerView, frame: pickerFrame) + } + + let unitLabelSize = self.unitLabel.update( + transition: transition, + component: AnyComponent(Text( + text: self.usesMetricSystem ? environment.strings.Location_ProximityNotification_DistanceKM : environment.strings.Location_ProximityNotification_DistanceMI, + font: Font.regular(15.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: titleHeight) + ) + if let unitLabelView = self.unitLabel.view { + if unitLabelView.superview == nil { + self.addSubview(unitLabelView) + } + transition.setFrame(view: unitLabelView, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0) + 50.0, y: floor(pickerFrame.midY - unitLabelSize.height / 2.0)), size: unitLabelSize)) + } + + let smallUnitLabelSize = self.smallUnitLabel.update( + transition: transition, + component: AnyComponent(Text( + text: self.usesMetricSystem ? environment.strings.Location_ProximityNotification_DistanceM : "", + font: Font.regular(15.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: titleHeight) + ) + if let smallUnitLabelView = self.smallUnitLabel.view { + if smallUnitLabelView.superview == nil { + self.addSubview(smallUnitLabelView) + } + transition.setFrame(view: smallUnitLabelView, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0 * 3.0) + 50.0, y: floor(pickerFrame.midY - smallUnitLabelSize.height / 2.0)), size: smallUnitLabelSize)) + } + + let bottomY = pickerFrame.maxY + 17.0 + var buttonTitle = environment.strings.Location_ProximityNotification_Notify(distanceText).string + if let displayTitle = component.compactDisplayTitle { + let longTitle = environment.strings.Location_ProximityNotification_NotifyLong(displayTitle, distanceText).string + let titleSize = NSAttributedString(string: longTitle, font: Font.semibold(17.0), textColor: .black).boundingRect(with: CGSize(width: availableSize.width * 2.0, height: 50.0), options: .usesLineFragmentOrigin, context: nil).size + if titleSize.width < availableSize.width - 70.0 { + buttonTitle = longTitle } } - self.doneButton.title = longTitle ?? shortTitle - - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Location_ProximityNotification_AlreadyClose(distance).string, font: Font.regular(14.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + + var buttonTransition = transition + if transition.animation.isImmediate { + buttonTransition = buttonTransition.withAnimation(.curve(duration: 0.2, curve: .easeInOut)) + } + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonFillColor, + foreground: buttonForegroundColor, + pressedColor: buttonFillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable("title"), component: AnyComponent( + AnimatedTextComponent( + font: Font.semibold(17.0), + color: buttonForegroundColor, + items: [ + AnimatedTextComponent.Item(id: AnyHashable("title"), content: .text(buttonTitle)) + ], + noDelay: true + ) + )), + isEnabled: !isTooFar && !self.isCompleting, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedDistance = self.selectedDistance() else { + return + } + self.isCompleting = true + self.state?.updated(transition: .immediate) + component.completion(selectedDistance.convertedValue) + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: bottomY), size: buttonSize)) + buttonTransition.setAlpha(view: buttonView, alpha: isTooFar ? 0.0 : 1.0) } - var convertedValue = Double(value) - if !self.usesMetricSystem { - convertedValue = Double(convertedValue) * 1.60934 + let warningSize = self.warningText.update( + transition: transition, + component: AnyComponent(Text( + text: environment.strings.Location_ProximityNotification_AlreadyClose(distanceText).string, + font: Font.regular(14.0), + color: secondaryTextColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + ) + if let warningTextView = self.warningText.view { + if warningTextView.superview == nil { + self.addSubview(warningTextView) + } + transition.setFrame(view: warningTextView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - warningSize.width) * 0.5), y: bottomY + floorToScreenPixels((buttonHeight - warningSize.height) * 0.5)), size: warningSize)) + buttonTransition.setAlpha(view: warningTextView, alpha: isTooFar ? 1.0 : 0.0) } - if let distance = self.distances.last, convertedValue > distance { - self.doneButton.alpha = 0.0 - self.doneButton.isUserInteractionEnabled = false - self.textNode.alpha = 1.0 + return CGSize(width: availableSize.width, height: bottomY + buttonHeight + buttonInsets.bottom) + } + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 2 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + if component == 0 { + return unitValues.count + } else if component == 1 { + return smallUnitValues.count } else { - self.doneButton.alpha = 1.0 - self.doneButton.isUserInteractionEnabled = true - self.textNode.alpha = 0.0 + return 1 } } - } - - var previousReportedValue: Int32? - fileprivate func update() -> Bool { - if let pickerView = self.pickerView { - let selectedLargeRow = pickerView.selectedRow(inComponent: 0) - var selectedSmallRow = pickerView.selectedRow(inComponent: 1) - if selectedLargeRow == 0 && selectedSmallRow == 0 { - selectedSmallRow = 1 + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + guard let environment = self.environment else { + return nil } - let largeValue = unitValues[selectedLargeRow] - let smallValue = smallUnitValues[selectedSmallRow] - - var value = largeValue * 1000 + smallValue * 10 - if !self.usesMetricSystem { - value = Int32(Double(value) * 1.60934) - } - - if let previousReportedValue = self.previousReportedValue, value == previousReportedValue { - return false - } else { - self.updated?(value) - self.previousReportedValue = value - return true - } - } else { - return false - } - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if pickerView.selectedRow(inComponent: 0) == 0 && pickerView.selectedRow(inComponent: 1) == 0 { - pickerView.selectRow(1, inComponent: 1, animated: true) - } - self.updateDoneButtonTitle() - let _ = self.update() - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - if component == 0 { - return unitValues.count - } else if component == 1 { - return smallUnitValues.count - } else { - return 1 - } - } - - func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { - let font = Font.regular(17.0) - let string: String - if component == 0 { - let value = unitValues[row] - string = "\(value)" - } else { - if self.usesMetricSystem { - let value = String(format: "%d", smallUnitValues[row] * 10) + let font = Font.regular(17.0) + let string: String + if component == 0 { + let value = unitValues[row] string = "\(value)" } else { - let value = smallUnitValues[row] - if value == 0 { - string = ".0" - } else if value == 5 { - string = ".05" + if self.usesMetricSystem { + let value = String(format: "%d", smallUnitValues[row] * 10) + string = "\(value)" } else { - string = ".\(value / 10)" + let value = smallUnitValues[row] + if value == 0 { + string = ".0" + } else if value == 5 { + string = ".05" + } else { + string = ".\(value / 10)" + } } } - } - return NSAttributedString(string: string, font: font, textColor: self.presentationData.theme.actionSheet.primaryTextColor) - } - - func updatePresentationData(_ presentationData: PresentationData) { - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - guard case .default = self.controllerStyle else { - return - } - - if let effectView = self.effectNode.view as? UIVisualEffectView { - effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.setupPickerView() - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - - self.updateDoneButtonTitle() - - self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - let offset = self.contentContainerNode.frame.height - let position = self.wrappingScrollNode.position - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - self.wrappingScrollNode.position = CGPoint(x: position.x, y: position.y + offset) - transition.animateView({ - self.wrappingScrollNode.position = position - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() + + let textColor: UIColor + switch self.component?.style { + case .media: + textColor = .white + default: + textColor = environment.theme.actionSheet.primaryTextColor } - completion?() + return NSAttributedString(string: string, font: font, textColor: textColor) } - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.contentContainerNode.frame.height - self.wrappingScrollNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + if pickerView.selectedRow(inComponent: 0) == 0 && pickerView.selectedRow(inComponent: 1) == 0 { + pickerView.selectRow(1, inComponent: 1, animated: true) } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() + let _ = self.reportSelectedValue() + self.state?.updated(transition: .immediate) } } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let hadValidLayout = self.containerLayout != nil - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let buttonOffset: CGFloat = 0.0 - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 - let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight) - contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top + 66.0 + UIScreenPixel))) - - let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight) - transition.updateFrame(node: self.doneButton, frame: doneButtonFrame) - - let textSize = self.textNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor(doneButtonFrame.center.y - textSize.height / 2.0)), size: textSize)) - - let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) - self.pickerView?.frame = pickerFrame - - let unitLabelSize = self.unitLabelNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.unitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0) + 50.0, y: floor(pickerFrame.center.y - unitLabelSize.height / 2.0)), size: unitLabelSize)) - - let smallUnitLabelSize = self.smallUnitLabelNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.smallUnitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0 * 3.0) + 50.0, y: floor(pickerFrame.center.y - smallUnitLabelSize.height / 2.0)), size: smallUnitLabelSize)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - - if !hadValidLayout { - self.updateDoneButtonTitle() - } + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 032f1f621c..d87f8c99a1 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -7,9 +7,10 @@ import TelegramCore import TelegramPresentationData import ItemListUI import LocationResources -import AppBundle -import SolidRoundedButtonNode import ShimmerEffect +import ComponentFlow +import ButtonComponent +import BundleIconComponent public final class LocationInfoListItem: ListViewItem { let presentationData: ItemListPresentationData @@ -18,27 +19,35 @@ public final class LocationInfoListItem: ListViewItem { let address: String? let distance: String? let drivingTime: ExpectedTravelTime - let transitTime: ExpectedTravelTime let walkingTime: ExpectedTravelTime let hasEta: Bool let action: () -> Void let drivingAction: () -> Void - let transitAction: () -> Void let walkingAction: () -> Void - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool, action: @escaping () -> Void, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { + public init( + presentationData: ItemListPresentationData, + engine: TelegramEngine, + location: TelegramMediaMap, + address: String?, + distance: String?, + drivingTime: ExpectedTravelTime, + walkingTime: ExpectedTravelTime, + hasEta: Bool, + action: @escaping () -> Void, + drivingAction: @escaping () -> Void, + walkingAction: @escaping () -> Void + ) { self.presentationData = presentationData self.engine = engine self.location = location self.address = address self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.hasEta = hasEta self.action = action self.drivingAction = drivingAction - self.transitAction = transitAction self.walkingAction = walkingAction } @@ -76,31 +85,26 @@ public final class LocationInfoListItem: ListViewItem { } public final class LocationInfoListItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? private let venueIconNode: TransformImageNode private let buttonNode: HighlightableButtonNode private var placeholderNode: ShimmerEffectNode? - private var drivingButtonNode: SolidRoundedButtonNode? - private var transitButtonNode: SolidRoundedButtonNode? - private var walkingButtonNode: SolidRoundedButtonNode? + private let drivingButton = ComponentView() + private let walkingButton = ComponentView() private var item: LocationInfoListItem? private var layoutParams: ListViewItemLayoutParams? private var absoluteLocation: (CGRect, CGSize)? required public init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true self.buttonNode = HighlightableButtonNode() self.venueIconNode = TransformImageNode() self.venueIconNode.isUserInteractionEnabled = false super.init(layerBacked: false, rotated: false, seeThrough: false) - //self.addSubnode(self.backgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.venueIconNode) @@ -145,14 +149,15 @@ public final class LocationInfoListItemNode: ListViewItemNode { let iconLayout = self.venueIconNode.asyncLayout() return { [weak self] item, params in - let leftInset: CGFloat = 75.0 + params.leftInset + let leftInset: CGFloat = 78.0 + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 14.0 - let iconSize: CGFloat = 48.0 - let inset: CGFloat = 15.0 + let iconSize: CGFloat = 40.0 + let directionsButtonHeight: CGFloat = 52.0 + let directionsTopInset: CGFloat = 18.0 - let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) - let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let title: String let subtitle: String @@ -179,10 +184,11 @@ public final class LocationInfoListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 - let bottomInset: CGFloat = 4.0 + let titleSpacing: CGFloat = 0.0 + let bottomInset: CGFloat = 16.0 let textContentSize = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset - let contentSize = CGSize(width: params.width, height: item.hasEta ? max(100.0, textContentSize) : textContentSize) + let etaContentSize = verticalInset + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + directionsTopInset + directionsButtonHeight + bottomInset + let contentSize = CGSize(width: params.width, height: item.hasEta ? max(etaContentSize, textContentSize) : textContentSize) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (nodeLayout, { [weak self] in @@ -200,13 +206,7 @@ public final class LocationInfoListItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - } - - strongSelf.backgroundNode.isHidden = params.isStandalone - + let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) if let updatedLocation = updatedLocation { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true)) @@ -229,107 +229,126 @@ public final class LocationInfoListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } - let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - if strongSelf.drivingButtonNode == nil { - strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.drivingButtonNode?.iconSpacing = 5.0 - strongSelf.drivingButtonNode?.alpha = 0.0 - strongSelf.drivingButtonNode?.allowsGroupOpacity = true - strongSelf.drivingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.drivingAction() - } - } - strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.transitButtonNode?.iconSpacing = 2.0 - strongSelf.transitButtonNode?.alpha = 0.0 - strongSelf.transitButtonNode?.allowsGroupOpacity = true - strongSelf.transitButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.transitAction() - } - } - strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.walkingButtonNode?.iconSpacing = 2.0 - strongSelf.walkingButtonNode?.alpha = 0.0 - strongSelf.walkingButtonNode?.allowsGroupOpacity = true - strongSelf.walkingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.walkingAction() - } - } - strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } - } else if let _ = updatedTheme { - strongSelf.drivingButtonNode?.updateTheme(buttonTheme) - strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.transitButtonNode?.updateTheme(buttonTheme) - strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.walkingButtonNode?.updateTheme(buttonTheme) - strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) subtitleNode.frame = subtitleFrame - let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize)) + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 26.0, y: 14.0), size: CGSize(width: iconSize, height: iconSize)) strongSelf.venueIconNode.frame = iconNodeFrame - var directionsWidth: CGFloat = 93.0 + let glassInset: CGFloat = 6.0 + let buttonSideInset: CGFloat = 30.0 + let buttonSpacing: CGFloat = 10.0 + var directionsWidth: CGFloat = floorToScreenPixels((params.width - glassInset * 2.0 - buttonSideInset * 2.0 - buttonSpacing) / 2.0) if item.hasEta { - if item.drivingTime == .unknown && item.transitTime == .unknown && item.walkingTime == .unknown { - strongSelf.drivingButtonNode?.icon = nil - strongSelf.drivingButtonNode?.title = item.presentationData.strings.Map_GetDirections - if let drivingButtonNode = strongSelf.drivingButtonNode { - let buttonSize = drivingButtonNode.sizeThatFits(contentSize) - directionsWidth = buttonSize.width - } + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + + var drivingButtonTitle = "" + var walkingButtonTitle = "" + var drivingButtonHasIcon = true + var drivingButtonVisible = false + var walkingButtonVisible = false + + if item.drivingTime == .unknown && item.walkingTime == .unknown { + drivingButtonHasIcon = false + drivingButtonTitle = item.presentationData.strings.Map_GetDirections + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { if case let .ready(drivingTime) = item.drivingTime { - strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? "" + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - - if case let .ready(transitTime) = item.transitTime { - strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) - - if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime { - strongSelf.transitButtonNode?.alpha = 1.0 - strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } if case let .ready(walkingTime) = item.walkingTime { - strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? "" + walkingButtonVisible = true if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime { - strongSelf.walkingButtonNode?.alpha = 1.0 - strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.walkingButton.view?.alpha = 1.0 + strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } + + let drivingButtonContent: AnyComponent + if drivingButtonHasIcon { + drivingButtonContent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))) + ], spacing: 5.0) + ) + } else { + drivingButtonContent = AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)) + } - let directionsSpacing: CGFloat = 8.0 + let drivingButtonSize = strongSelf.drivingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("driving-\(drivingButtonHasIcon)-\(drivingButtonTitle)"), + component: drivingButtonContent + ), + action: { [weak self] in + if let item = self?.item { + item.drivingAction() + } + } + )), + environment: {}, + containerSize: CGSize(width: drivingButtonHasIcon ? directionsWidth : contentSize.width - glassInset * 2.0 - buttonSideInset * 2.0, height: directionsButtonHeight) + ) + if !drivingButtonHasIcon { + directionsWidth = drivingButtonSize.width + } - if case .calculating = item.drivingTime, case .calculating = item.transitTime, case .calculating = item.walkingTime { + let walkingButtonSize = strongSelf.walkingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("walking-\(walkingButtonTitle)"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))) + ], spacing: 2.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + )), + environment: {}, + containerSize: CGSize(width: directionsWidth, height: directionsButtonHeight) + ) + + var buttonOrigin = glassInset + buttonSideInset + + if case .calculating = item.drivingTime, case .calculating = item.walkingTime { let shimmerNode: ShimmerEffectNode if let current = strongSelf.placeholderNode { shimmerNode = current @@ -338,17 +357,17 @@ public final class LocationInfoListItemNode: ListViewItemNode { strongSelf.placeholderNode = shimmerNode strongSelf.addSubnode(shimmerNode) } - shimmerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: contentSize.width - leftInset, height: 32.0)) + shimmerNode.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: contentSize.width - buttonOrigin * 2.0, height: directionsButtonHeight)) if let (rect, size) = strongSelf.absoluteLocation { shimmerNode.updateAbsoluteRect(rect, within: size) } var shapes: [ShimmerEffectNode.Shape] = [] - shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: 32.0)) - shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0)) - shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing + directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) - shimmerNode.update(backgroundColor: item.presentationData.theme.list.plainBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size) + shimmerNode.update(backgroundColor: .clear, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size, mask: true) } else if let shimmerNode = strongSelf.placeholderNode { strongSelf.placeholderNode = nil shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerNode] _ in @@ -356,30 +375,42 @@ public final class LocationInfoListItemNode: ListViewItemNode { }) } - let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - var buttonOrigin = leftInset - strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: drivingButtonSize.height)) + if let drivingButtonView = strongSelf.drivingButton.view { + if drivingButtonView.superview == nil { + strongSelf.view.addSubview(drivingButtonView) + } + drivingButtonView.frame = drivingButtonFrame + if drivingButtonView.layer.animation(forKey: "opacity") == nil { + drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0 + } + } if case .ready = item.drivingTime { - buttonOrigin += directionsWidth + directionsSpacing + buttonOrigin += directionsWidth + buttonSpacing } - strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) - - if case .ready = item.transitTime { - buttonOrigin += directionsWidth + directionsSpacing + let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: walkingButtonSize.height)) + if let walkingButtonView = strongSelf.walkingButton.view { + if walkingButtonView.superview == nil { + strongSelf.view.addSubview(walkingButtonView) + } + walkingButtonView.frame = walkingButtonFrame + if walkingButtonView.layer.animation(forKey: "opacity") == nil { + walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0 + } } - - strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) } else { - + strongSelf.drivingButton.view?.alpha = 0.0 + strongSelf.walkingButton.view?.alpha = 0.0 + if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0) - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) } }) }) diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 5aaaf511fc..be96523c7c 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -10,10 +10,11 @@ import TelegramUIPreferences import TelegramStringFormatting import ItemListUI import LocationResources -import AppBundle import AvatarNode import LiveLocationTimerNode -import SolidRoundedButtonNode +import ComponentFlow +import ButtonComponent +import BundleIconComponent final class LocationLiveListItem: ListViewItem { let presentationData: ItemListPresentationData @@ -24,17 +25,15 @@ final class LocationLiveListItem: ListViewItem { let distance: Double? let drivingTime: ExpectedTravelTime - let transitTime: ExpectedTravelTime let walkingTime: ExpectedTravelTime let action: () -> Void let longTapAction: () -> Void let drivingAction: () -> Void - let transitAction: () -> Void let walkingAction: () -> Void - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -42,12 +41,10 @@ final class LocationLiveListItem: ListViewItem { self.message = message self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.action = action self.longTapAction = longTapAction self.drivingAction = drivingAction - self.transitAction = transitAction self.walkingAction = walkingAction } @@ -91,28 +88,19 @@ final class LocationLiveListItem: ListViewItem { private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) final class LocationLiveListItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? private let avatarNode: AvatarNode private var timerNode: ChatMessageLiveLocationTimerNode? - private var drivingButtonNode: SolidRoundedButtonNode? - private var transitButtonNode: SolidRoundedButtonNode? - private var walkingButtonNode: SolidRoundedButtonNode? + private let drivingButton = ComponentView() + private let walkingButton = ComponentView() private var item: LocationLiveListItem? private var layoutParams: ListViewItemLayoutParams? required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -121,8 +109,6 @@ final class LocationLiveListItemNode: ListViewItemNode { super.init(layerBacked: false, rotated: false, seeThrough: false) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) } @@ -142,7 +128,7 @@ final class LocationLiveListItemNode: ListViewItemNode { if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + self.insertSubnode(self.highlightedBackgroundNode, at: 0) } } else { if self.highlightedBackgroundNode.supernode != nil { @@ -169,7 +155,7 @@ final class LocationLiveListItemNode: ListViewItemNode { let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) return { [weak self] item, params, hasSeparator in - let leftInset: CGFloat = 65.0 + params.leftInset + let leftInset: CGFloat = 72.0 + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 8.0 @@ -203,18 +189,17 @@ final class LocationLiveListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 + let titleSpacing: CGFloat = 0.0 var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height) - let hasEta: Bool + var hasEta: Bool if case .ready = item.drivingTime { hasEta = true - } else if case .ready = item.transitTime { - hasEta = true } else if case .ready = item.walkingTime { hasEta = true } else { hasEta = false } + hasEta = true if hasEta { contentSize.height += 46.0 } @@ -232,9 +217,7 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.layoutParams = params if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor } let titleNode = titleApply() @@ -249,72 +232,23 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } - let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - if strongSelf.drivingButtonNode == nil { - strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.drivingButtonNode?.alpha = 0.0 - strongSelf.drivingButtonNode?.iconSpacing = 5.0 - strongSelf.drivingButtonNode?.allowsGroupOpacity = true - strongSelf.drivingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.drivingAction() - } - } - strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.transitButtonNode?.alpha = 0.0 - strongSelf.transitButtonNode?.iconSpacing = 2.0 - strongSelf.transitButtonNode?.allowsGroupOpacity = true - strongSelf.transitButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.transitAction() - } - } - strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.walkingButtonNode?.alpha = 0.0 - strongSelf.walkingButtonNode?.iconSpacing = 2.0 - strongSelf.walkingButtonNode?.allowsGroupOpacity = true - strongSelf.walkingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.walkingAction() - } - } - strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } - } else if let _ = updatedTheme { - strongSelf.drivingButtonNode?.updateTheme(buttonTheme) - strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.transitButtonNode?.updateTheme(buttonTheme) - strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.walkingButtonNode?.updateTheme(buttonTheme) - strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) subtitleNode.frame = subtitleFrame - let separatorHeight = UIScreenPixel - let topHighlightInset: CGFloat = separatorHeight - let separatorRightInset: CGFloat = 16.0 let avatarSize: CGFloat = 40.0 - if let peer = item.message.author { strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false) } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 22.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize)) - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight)) - strongSelf.separatorNode.isHidden = !hasSeparator + let highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0)) + let highlightCornerRadius = highlightFrame.height * 0.5 + strongSelf.highlightedBackgroundNode.frame = highlightFrame + strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius var liveBroadcastingTimeout: Int32 = 0 if let location = getLocation(from: item.message), let timeout = location.liveBroadcastingTimeout { @@ -338,61 +272,134 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.addSubnode(timerNode) strongSelf.timerNode = timerNode } - let timerSize = CGSize(width: 28.0, height: 28.0) + let timerSize = CGSize(width: 24.0, height: 24.0) timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings) - timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize) + timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 26.0 - timerSize.width, y: floorToScreenPixels((56.0 - timerSize.height) / 2.0)), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() } + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + var directionsSize = CGSize(width: 96.0, height: 36.0) + let directionsSpacing: CGFloat = 8.0 + + var drivingButtonTitle = "" + var drivingButtonHasIcon = true + var walkingButtonTitle = "" + var drivingButtonVisible = false + var walkingButtonVisible = false + if case let .ready(drivingTime) = item.drivingTime { - strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? "" + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - } - - if case let .ready(transitTime) = item.transitTime { - strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) - - if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime { - strongSelf.transitButtonNode?.alpha = 1.0 - strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + drivingButtonVisible = true + if case .unknown = item.walkingTime { + drivingButtonHasIcon = false + drivingButtonTitle = item.presentationData.strings.Map_GetDirections + directionsSize.width = contentSize.width - leftInset * 2.0 } } if case let .ready(walkingTime) = item.walkingTime { - strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? "" + walkingButtonVisible = true if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime { - strongSelf.walkingButtonNode?.alpha = 1.0 - strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.walkingButton.view?.alpha = 1.0 + strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let directionsWidth: CGFloat = 93.0 - let directionsSpacing: CGFloat = 8.0 - let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + var drivingButtonContent: [AnyComponentWithIdentity] = [] + if drivingButtonHasIcon { + drivingButtonContent.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor)))) + } + drivingButtonContent.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(14.0), color: foregroundColor)))) + + let drivingButtonSize = strongSelf.drivingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("driving-\(drivingButtonTitle)"), + component: AnyComponent( + HStack(drivingButtonContent, spacing: 2.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.drivingAction() + } + } + )), + environment: {}, + containerSize: directionsSize + ) + + let walkingButtonSize = strongSelf.walkingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("walking-\(walkingButtonTitle)"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(14.0), color: foregroundColor))) + ], spacing: 0.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + )), + environment: {}, + containerSize: directionsSize + ) var buttonOrigin = leftInset - strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: drivingButtonSize) + if let drivingButtonView = strongSelf.drivingButton.view { + if drivingButtonView.superview == nil { + strongSelf.view.addSubview(drivingButtonView) + } + drivingButtonView.frame = drivingButtonFrame + if drivingButtonView.layer.animation(forKey: "opacity") == nil { + drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0 + } + } if case .ready = item.drivingTime { - buttonOrigin += directionsWidth + directionsSpacing + buttonOrigin += directionsSize.width + directionsSpacing } - strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) - - if case .ready = item.transitTime { - buttonOrigin += directionsWidth + directionsSpacing + let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: walkingButtonSize) + if let walkingButtonView = strongSelf.walkingButton.view { + if walkingButtonView.superview == nil { + strongSelf.view.addSubview(walkingButtonView) + } + walkingButtonView.frame = walkingButtonFrame + if walkingButtonView.layer.animation(forKey: "opacity") == nil { + walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0 + } } - - strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) } }) }) diff --git a/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift b/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift new file mode 100644 index 0000000000..d79e5bb5e1 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift @@ -0,0 +1,68 @@ +import UIKit +import Display +import ContextUI +import TelegramPresentationData +import TelegramCore + +private final class LocationLiveLocationReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +func makeLiveLocationDurationContextController( + presentationData: PresentationData, + sourceView: UIView, + title: String, + selectPeriod: @escaping (Int32) -> Void +) -> ViewController { + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem( + text: title, + textLayout: .multiline, + textFont: .small, + parseMarkdown: true, + icon: { _ in + return nil + }, + action: noAction + ))) + + items.append(.separator) + + let periodItems: [(String, Int32)] = [ + (presentationData.strings.Map_LiveLocationForMinutes(15), 15 * 60), + (presentationData.strings.Map_LiveLocationForHours(1), 60 * 60 - 1), + (presentationData.strings.Map_LiveLocationForHours(8), 8 * 60 * 60), + (presentationData.strings.Map_LiveLocationIndefinite, liveLocationIndefinitePeriod) + ] + + for (text, period) in periodItems { + items.append(.action(ContextMenuActionItem( + text: text, + icon: { _ in + return nil + }, + action: { _, f in + f(.default) + selectPeriod(period) + } + ))) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(LocationLiveLocationReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + return contextController +} diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index c3ef2e37d1..7645090d71 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -2,6 +2,9 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import AccountContext +import TelegramCore +import SwiftSignalKit import TelegramPresentationData import AppBundle import CoreLocation @@ -10,6 +13,8 @@ import GlassBackgroundComponent import PlainButtonComponent import BundleIconComponent import MultilineTextComponent +import LottieComponent +import LottieComponentResourceContent private let panelInset: CGFloat = 4.0 private let panelButtonSize = CGSize(width: 46.0, height: 46.0) @@ -48,6 +53,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let goToUserLocation: () -> Void private let showPlacesInThisArea: () -> Void private let setupProximityNotification: (Bool) -> Void + private let weatherPressed: () -> Void private var displayingPlacesButton = false private var proximityNotification: Bool? @@ -67,10 +73,20 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let placesBackgroundView: GlassBackgroundView? private let placesBackgroundNode: ASImageNode private let placesButtonNode: HighlightTrackingButtonNode - private let shadowNode: ASImageNode + private let weatherBackgroundView: GlassBackgroundView? + private let weatherIcon = ComponentView() + private let weatherEmojiLabel = ComponentView() + private let weatherTemperatureLabel = ComponentView() + private let weatherButton: HighlightTrackingButton + private var weatherEmoji: String? + private var weatherTemperature: String? + private var weatherEmojiFile: TelegramMediaFile? + private weak var weatherContext: AccountContext? + private let weatherEmojiLoadDisposable = MetaDisposable() + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)? - + public init( presentationData: PresentationData, glass: Bool, @@ -79,7 +95,8 @@ public final class LocationMapHeaderNode: ASDisplayNode { updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, - showPlacesInThisArea: @escaping () -> Void = {} + showPlacesInThisArea: @escaping () -> Void = {}, + weatherPressed: @escaping () -> Void = {} ) { self.presentationData = presentationData self.glass = glass @@ -88,6 +105,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.goToUserLocation = goToUserLocation self.setupProximityNotification = setupProximityNotification self.showPlacesInThisArea = showPlacesInThisArea + self.weatherPressed = weatherPressed self.mapNode = LocationMapNode() @@ -131,29 +149,35 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.placesBackgroundNode.displayWithoutProcessing = true self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.placesBackgroundNode.isUserInteractionEnabled = true - + self.placesButtonNode = HighlightTrackingButtonNode() self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.medium(17.0), with: self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor : buttonColor, for: .normal) - - self.shadowNode = ASImageNode() - self.shadowNode.contentMode = .scaleToFill - self.shadowNode.displaysAsynchronously = false - self.shadowNode.displayWithoutProcessing = true - self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) - + + self.weatherButton = HighlightTrackingButton() + if glass { self.optionsBackgroundView = GlassContextExtractableContainer() self.optionsBackgroundNode.image = nil self.placesBackgroundView = GlassBackgroundView() self.placesBackgroundNode.image = nil + + self.weatherBackgroundView = GlassBackgroundView() } else { self.optionsBackgroundView = nil self.placesBackgroundView = nil + self.weatherBackgroundView = nil } - + super.init() - + + self.mapNode.visibleRegionDidChange = { [weak self] in + guard let self, self.glass, self.mapNode.mapMode == .satellite else { + return + } + self.requestLayout(transition: .immediate) + } + self.clipsToBounds = true self.addSubnode(self.mapNode) @@ -176,15 +200,28 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view) } } - + self.addSubnode(self.placesBackgroundNode) self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view) - //self.addSubnode(self.shadowNode) - + if let weatherBackgroundView = self.weatherBackgroundView { + weatherBackgroundView.isHidden = true + self.view.addSubview(weatherBackgroundView) + } + self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) self.locationButtonNode.addTarget(self, action: #selector(self.locationPressed), forControlEvents: .touchUpInside) self.notificationButtonNode.addTarget(self, action: #selector(self.notificationPressed), forControlEvents: .touchUpInside) self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside) + + self.weatherButton.addTarget(self, action: #selector(self.weatherButtonPressed), for: .touchUpInside) + } + + deinit { + self.weatherEmojiLoadDisposable.dispose() + } + + @objc private func weatherButtonPressed() { + self.weatherPressed() } public func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { @@ -214,6 +251,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { let buttonColor = self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62) : presentationData.theme.rootController.navigationBar.buttonColor + self.mapNode.isDark = presentationData.theme.overallDarkAppearance self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor self.optionsSecondSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor @@ -227,9 +265,45 @@ public final class LocationMapHeaderNode: ASDisplayNode { if !self.glass { self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) } - self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) } + func updateWeatherData(context: AccountContext, emoji: String, temperature: String, animated: Bool) { + let emojiFile = context.animatedEmojiStickersValue[emoji]?.first?.file._parse() + if self.weatherEmoji == emoji && self.weatherTemperature == temperature && self.weatherEmojiFile?.fileId == emojiFile?.fileId { + return + } + + if self.weatherEmojiFile?.fileId != emojiFile?.fileId { + if let emojiFile { + self.weatherEmojiLoadDisposable.set(context.engine.resources.fetch(reference: .standalone(resource: emojiFile.resource), userLocation: .other, userContentType: .sticker).start()) + } else { + self.weatherEmojiLoadDisposable.set(nil) + } + } + + self.weatherContext = context + self.weatherEmoji = emoji + self.weatherTemperature = temperature + self.weatherEmojiFile = emojiFile + self.requestLayout(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + + func clearWeatherData(animated: Bool) { + if self.weatherEmoji == nil && self.weatherTemperature == nil { + return + } + self.weatherEmoji = nil + self.weatherTemperature = nil + self.weatherEmojiFile = nil + self.weatherContext = nil + self.weatherEmojiLoadDisposable.set(nil) + if let weatherIconView = self.weatherIcon.view as? LottieComponent.View { + weatherIconView.externalShouldPlay = false + } + self.weatherIcon.view?.removeFromSuperview() + self.requestLayout(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + private func iconForTracking() -> UIImage? { switch self.trackingMode { case .none: @@ -241,7 +315,23 @@ public final class LocationMapHeaderNode: ASDisplayNode { } } - public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, controlsTopPadding: CGFloat, controlsBottomPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { + func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationBarHeight, topPadding, controlsTopPadding, controlsBottomPadding, offset, size) = self.validLayout else { + return + } + self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, controlsTopPadding: controlsTopPadding, controlsBottomPadding: controlsBottomPadding, offset: offset, size: size, transition: transition) + } + + public func updateLayout( + layout: ContainerViewLayout, + navigationBarHeight: CGFloat, + topPadding: CGFloat, + controlsTopPadding: CGFloat, + controlsBottomPadding: CGFloat, + offset: CGFloat, + size: CGSize, + transition: ContainedViewLayoutTransition + ) { self.validLayout = (layout, navigationBarHeight, topPadding, controlsTopPadding, controlsBottomPadding, offset, size) let mapHeight: CGFloat = floor(layout.size.height * 1.3) + layout.intrinsicInsets.top * 2.0 @@ -265,8 +355,126 @@ public final class LocationMapHeaderNode: ASDisplayNode { transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) - transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0)) - + if let weatherBackgroundView = self.weatherBackgroundView { + let panelHeight: CGFloat = 36.0 + let horizontalInset: CGFloat = 10.0 + let labelSpacing: CGFloat = 5.0 + let iconSize = CGSize(width: floor(panelHeight * 0.71), height: floor(panelHeight * 0.71)) + let componentTransition = ComponentTransition(transition) + let temperatureSize = self.weatherTemperatureLabel.update( + transition: componentTransition, + component: AnyComponent(Text( + text: self.weatherTemperature ?? "", + font: Font.semibold(15.0), + color: self.presentationData.theme.rootController.navigationBar.primaryTextColor + )), + environment: {}, + containerSize: CGSize(width: 72.0, height: panelHeight) + ) + let emojiSize = self.weatherEmojiLabel.update( + transition: componentTransition, + component: AnyComponent(Text( + text: self.weatherEmoji ?? "", + font: Font.regular(18.0), + color: self.presentationData.theme.rootController.navigationBar.primaryTextColor + )), + environment: {}, + containerSize: iconSize + ) + let panelWidth = max(62.0, ceil(horizontalInset * 2.0 + iconSize.width + labelSpacing + temperatureSize.width)) + let panelFrame = CGRect( + x: layout.safeInsets.left + 16.0, + y: navigationBarHeight + topPadding + 14.0, + width: panelWidth, + height: panelHeight + ) + transition.updateFrame(view: weatherBackgroundView, frame: panelFrame) + + let weatherAlpha: CGFloat = self.weatherEmoji != nil && self.weatherTemperature != nil && size.height > 160.0 + navigationBarHeight && !self.forceIsHidden ? 1.0 : 0.0 + if weatherBackgroundView.isHidden && weatherAlpha > 0.0 { + weatherBackgroundView.isHidden = false + } + weatherBackgroundView.update(size: panelFrame.size, cornerRadius: panelFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, isVisible: weatherAlpha > 0.0, transition: ComponentTransition(transition)) + + let iconFrame = CGRect( + x: horizontalInset, + y: floorToScreenPixels((panelHeight - iconSize.height) / 2.0), + width: iconSize.width, + height: iconSize.height + ) + let temperatureFrame = CGRect( + x: iconFrame.maxX + labelSpacing, + y: floorToScreenPixels((panelHeight - temperatureSize.height) / 2.0), + width: temperatureSize.width, + height: temperatureSize.height + ) + let emojiFrame = CGRect( + x: iconFrame.minX + floorToScreenPixels((iconFrame.width - emojiSize.width) / 2.0), + y: iconFrame.minY + floorToScreenPixels((iconFrame.height - emojiSize.height) / 2.0), + width: emojiSize.width, + height: emojiSize.height + ) + if let weatherEmojiView = self.weatherEmojiLabel.view { + if weatherEmojiView.superview == nil { + weatherBackgroundView.contentView.addSubview(weatherEmojiView) + } + transition.updateFrame(view: weatherEmojiView, frame: emojiFrame) + } + if let weatherTemperatureView = self.weatherTemperatureLabel.view { + if weatherTemperatureView.superview == nil { + weatherBackgroundView.contentView.addSubview(weatherTemperatureView) + } + transition.updateFrame(view: weatherTemperatureView, frame: temperatureFrame) + } + + if let weatherContext = self.weatherContext, let weatherEmojiFile = self.weatherEmojiFile { + var weatherIconTransition = transition + let _ = self.weatherIcon.update( + transition: ComponentTransition(transition), + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent(context: weatherContext, file: weatherEmojiFile, attemptSynchronously: false, providesPlaceholder: true), + placeholderColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.1), + renderingScale: 2.0, + loop: true + ) + ), + environment: {}, + containerSize: iconSize + ) + if let weatherIconView = self.weatherIcon.view { + if weatherIconView.superview == nil { + weatherIconTransition = .immediate + weatherBackgroundView.contentView.addSubview(weatherIconView) + } + weatherIconTransition.updateFrame(view: weatherIconView, frame: iconFrame) + ComponentTransition(transition).setAlpha(view: weatherIconView, alpha: 1.0) + if let weatherIconView = weatherIconView as? LottieComponent.View { + weatherIconView.externalShouldPlay = weatherAlpha > 0.0 + } + } + if let weatherEmojiView = self.weatherEmojiLabel.view { + componentTransition.setAlpha(view: weatherEmojiView, alpha: 0.0) + } + } else { + if let weatherIconView = self.weatherIcon.view { + componentTransition.setAlpha(view: weatherIconView, alpha: 0.0) + if let weatherIconView = weatherIconView as? LottieComponent.View { + weatherIconView.externalShouldPlay = false + } + } + if let weatherEmojiView = self.weatherEmojiLabel.view { + componentTransition.setAlpha(view: weatherEmojiView, alpha: 1.0) + } + } + componentTransition.setAlpha(view: weatherBackgroundView, alpha: weatherAlpha) + + if self.weatherButton.superview == nil { + weatherBackgroundView.contentView.addSubview(self.weatherButton) + } + self.weatherButton.frame = CGRect(origin: .zero, size: panelFrame.size) + } + if let options = self.options { let optionsSize = options.update( transition: ComponentTransition(transition), @@ -275,24 +483,35 @@ public final class LocationMapHeaderNode: ASDisplayNode { theme: self.presentationData.theme, strings: self.presentationData.strings, mapMode: self.mapNode.mapMode, + currentCoordinate: self.mapNode.mapCenterCoordinate, + trackingMode: self.mapNode.trackingMode, showMapModes: self.infoButtonNode.isSelected, + proximityNotification: self.proximityNotification, updateMapMode: { [weak self] mode in guard let self else { return } self.updateMapMode(mode) + self.requestLayout(transition: .immediate) }, goToUserLocation: { [weak self] in guard let self else { return } self.goToUserLocation() + self.requestLayout(transition: .immediate) }, requestedMapModes: { [weak self] in guard let self else { return } self.toggleMapModeSelection() + }, + setupProximityNotification: { [weak self] reset in + guard let self else { + return + } + self.setupProximityNotification(reset) } ) ), @@ -343,12 +562,17 @@ public final class LocationMapHeaderNode: ASDisplayNode { } } } - - public func updateHighlight(_ highlighted: Bool) { - self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted) - } - + public func proximityButtonFrame() -> CGRect? { + if let options = self.options { + guard let optionsView = options.view as? LocationOptionsComponent.View else { + return nil + } + return optionsView.proximityButtonFrame().flatMap { frame in + return optionsView.convert(frame, to: self.view) + } + } + if self.notificationButtonNode.alpha > 0.0 { return self.optionsBackgroundNode.view.convert(self.notificationButtonNode.frame, to: self.view) } else { @@ -376,34 +600,46 @@ public final class LocationMapHeaderNode: ASDisplayNode { } -public final class LocationOptionsComponent: Component { - public let theme: PresentationTheme - public let strings: PresentationStrings - public let mapMode: LocationMapMode - public let showMapModes: Bool - public let updateMapMode: (LocationMapMode) -> Void - public let goToUserLocation: () -> Void - public let requestedMapModes: () -> Void +final class LocationOptionsComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let mapMode: LocationMapMode + let currentCoordinate: CLLocationCoordinate2D? + let trackingMode: LocationTrackingMode + let showMapModes: Bool + let proximityNotification: Bool? + let updateMapMode: (LocationMapMode) -> Void + let goToUserLocation: () -> Void + let requestedMapModes: () -> Void + let setupProximityNotification: (Bool) -> Void - public init( + init( theme: PresentationTheme, strings: PresentationStrings, mapMode: LocationMapMode, + currentCoordinate: CLLocationCoordinate2D?, + trackingMode: LocationTrackingMode, showMapModes: Bool, + proximityNotification: Bool?, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, - requestedMapModes: @escaping () -> Void + requestedMapModes: @escaping () -> Void, + setupProximityNotification: @escaping (Bool) -> Void ) { self.theme = theme self.strings = strings self.mapMode = mapMode + self.currentCoordinate = currentCoordinate + self.trackingMode = trackingMode self.showMapModes = showMapModes + self.proximityNotification = proximityNotification self.updateMapMode = updateMapMode self.goToUserLocation = goToUserLocation self.requestedMapModes = requestedMapModes + self.setupProximityNotification = setupProximityNotification } - public static func ==(lhs: LocationOptionsComponent, rhs: LocationOptionsComponent) -> Bool { + static func ==(lhs: LocationOptionsComponent, rhs: LocationOptionsComponent) -> Bool { if lhs.theme !== rhs.theme { return false } @@ -413,13 +649,43 @@ public final class LocationOptionsComponent: Component { if lhs.mapMode != rhs.mapMode { return false } + if lhs.mapMode == .satellite && LocationOptionsComponent.satelliteIconName(coordinate: lhs.currentCoordinate) != LocationOptionsComponent.satelliteIconName(coordinate: rhs.currentCoordinate) { + return false + } + if lhs.trackingMode != rhs.trackingMode { + return false + } if lhs.showMapModes != rhs.showMapModes { return false } + if lhs.proximityNotification != rhs.proximityNotification { + return false + } return true } - public final class View: HighlightTrackingButton { + private static func satelliteIconName(coordinate: CLLocationCoordinate2D?) -> String { + guard let coordinate else { + return "Location/OptionGlobeEurope" + } + + var longitude = coordinate.longitude.truncatingRemainder(dividingBy: 360.0) + if longitude < -180.0 { + longitude += 360.0 + } else if longitude > 180.0 { + longitude -= 360.0 + } + + if longitude >= -170.0 && longitude < -25.0 { + return "Location/OptionGlobeAmerica" + } else if longitude >= -25.0 && longitude < 60.0 { + return "Location/OptionGlobeEurope" + } else { + return "Location/OptionGlobeAsia" + } + } + + final class View: HighlightTrackingButton { private let containerView: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView private let clippingView: UIView @@ -427,6 +693,7 @@ public final class LocationOptionsComponent: Component { private let collapsedContainerView = UIView() private var mapModeButton = ComponentView() private var trackingButton = ComponentView() + private var notificationButton = ComponentView() private let expandedContainerView = UIView() private let checkIcon = UIImageView() @@ -448,7 +715,7 @@ public final class LocationOptionsComponent: Component { self.addSubview(self.containerView) self.containerView.contentView.addSubview(self.backgroundView) - self.addSubview(self.clippingView) + self.backgroundView.contentView.addSubview(self.clippingView) self.clippingView.addSubview(self.collapsedContainerView) self.clippingView.addSubview(self.expandedContainerView) } @@ -566,7 +833,7 @@ public final class LocationOptionsComponent: Component { transition.setFrame(view: hybridButtonView, frame: hybridButtonFrame) } - let normalSize = CGSize(width: 40.0, height: 80.0) + let normalSize = CGSize(width: 40.0, height: component.proximityNotification != nil ? 120.0 : 80.0) let expandedFrame = CGRect(origin: .zero, size: expandedSize) let collapsedFrame = CGRect(origin: CGPoint(x: expandedSize.width - normalSize.width, y: expandedSize.height - normalSize.height), size: normalSize) @@ -575,17 +842,26 @@ public final class LocationOptionsComponent: Component { self.backgroundView.update(size: effectiveBackgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) transition.setFrame(view: self.backgroundView, frame: effectiveBackgroundFrame) - transition.setFrame(view: self.clippingView, frame: effectiveBackgroundFrame) + transition.setFrame(view: self.clippingView, frame: CGRect(origin: .zero, size: effectiveBackgroundFrame.size)) transition.setFrame(view: self.expandedContainerView, frame: expandedFrame.offsetBy(dx: effectiveBackgroundFrame.width - expandedFrame.width, dy: effectiveBackgroundFrame.height - expandedFrame.height)) transition.setFrame(view: self.collapsedContainerView, frame: collapsedFrame.offsetBy(dx: -effectiveBackgroundFrame.minX, dy: -effectiveBackgroundFrame.minY)) + var mapModeIconName: String + switch component.mapMode { + case .map: + mapModeIconName = "Location/OptionMap" + case .satellite: + mapModeIconName = LocationOptionsComponent.satelliteIconName(coordinate: component.currentCoordinate) + case .hybrid: + mapModeIconName = "Location/OptionHybrid" + } let mapModeButtonSize = self.mapModeButton.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( - BundleIconComponent(name: "Location/OptionMap", tintColor: component.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62)) + BundleIconComponent(name: mapModeIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) ), minSize: CGSize(width: 40.0, height: 40.0), action: { [weak self] in @@ -608,13 +884,22 @@ public final class LocationOptionsComponent: Component { } transition.setFrame(view: mapModeButtonView, frame: mapModeButtonFrame) } - + + let trackingModeIconName: String + switch component.trackingMode { + case .none: + trackingModeIconName = "Location/OptionLocate" + case .follow: + trackingModeIconName = "Location/OptionLocating" + case .followWithHeading: + trackingModeIconName = "Location/OptionTracking" + } let trackingButtonSize = self.trackingButton.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( - BundleIconComponent(name: "Location/OptionLocate", tintColor: component.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62)) + BundleIconComponent(name: trackingModeIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) ), minSize: CGSize(width: 40.0, height: 40.0), action: { [weak self] in @@ -638,6 +923,41 @@ public final class LocationOptionsComponent: Component { transition.setFrame(view: trackingButtonView, frame: trackingButtonFrame) } + if let proximityNotification = component.proximityNotification { + let notificationIconName = proximityNotification ? "Chat/Title Panels/MuteIcon" : "Location/NotificationIcon" + let notificationButtonSize = self.notificationButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + BundleIconComponent(name: notificationIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) + ), + minSize: CGSize(width: 40.0, height: 40.0), + action: { [weak self] in + guard let self, let component = self.component, let proximityNotification = component.proximityNotification else { + return + } + component.setupProximityNotification(proximityNotification) + }, + animateAlpha: true, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let notificationButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: notificationButtonSize) + if let notificationButtonView = self.notificationButton.view { + if notificationButtonView.superview == nil { + self.collapsedContainerView.addSubview(notificationButtonView) + } + transition.setFrame(view: notificationButtonView, frame: notificationButtonFrame) + transition.setAlpha(view: notificationButtonView, alpha: 1.0) + } + } else if let notificationButtonView = self.notificationButton.view { + transition.setAlpha(view: notificationButtonView, alpha: 0.0) + } + transition.setAlpha(view: self.collapsedContainerView, alpha: component.showMapModes ? 0.0 : 1.0) transition.setAlpha(view: self.expandedContainerView, alpha: component.showMapModes ? 1.0 : 0.0) @@ -650,6 +970,16 @@ public final class LocationOptionsComponent: Component { public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } + + func proximityButtonFrame() -> CGRect? { + guard let component = self.component, component.proximityNotification != nil, !component.showMapModes else { + return nil + } + guard let notificationButtonView = self.notificationButton.view, notificationButtonView.superview != nil, notificationButtonView.alpha > 0.0 else { + return nil + } + return notificationButtonView.convert(notificationButtonView.bounds, to: self) + } } public func makeView() -> View { diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index 072865eccd..35caf43aef 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -15,9 +15,9 @@ public enum LocationMapMode { var mapType: MKMapType { switch self { case .satellite: - return .satellite + return .satelliteFlyover case .hybrid: - return .hybrid + return .hybridFlyover default: return .standard } @@ -135,6 +135,7 @@ private func generateProximityDim(size: CGSize) -> UIImage { } protocol MKMapViewDelegateTarget: AnyObject { + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) @@ -155,6 +156,10 @@ private final class MKMapViewDelegateImpl: NSObject, MKMapViewDelegate { super.init() } + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + self.target?.mapViewDidChangeVisibleRegion(mapView) + } + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { self.target?.mapView(mapView, regionWillChangeAnimated: animated) } @@ -237,12 +242,41 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { return self.view as? LocationMapView } + private func updateUserHeadingTransform(mapHeading: CGFloat) { + if let heading = self.userHeading { + self.headingArrowView?.isHidden = false + self.headingArrowView?.transform = CGAffineTransform(rotationAngle: (heading - mapHeading) / 180.0 * CGFloat.pi) + } else { + self.headingArrowView?.isHidden = true + self.headingArrowView?.transform = CGAffineTransform.identity + } + } + + private func updateHeadingTransforms(mapHeading: CGFloat? = nil) { + guard let mapView = self.mapView else { + return + } + + let mapHeading = mapHeading ?? CGFloat(mapView.camera.heading) + self.updateUserHeadingTransform(mapHeading: mapHeading) + + for annotation in mapView.annotations { + if let annotationView = mapView.view(for: annotation) as? LocationPinAnnotationView { + annotationView.updateMapHeading(mapHeading) + } + } + + self.pickerAnnotationView?.updateMapHeading(mapHeading) + self.customUserLocationAnnotationView?.updateMapHeading(mapHeading) + } + var returnedToUserLocation = true var ignoreRegionChanges = false var isDragging = false var onTouch: (() -> Void)? var beganInteractiveDragging: (() -> Void)? var endedInteractiveDragging: ((CLLocationCoordinate2D) -> Void)? + var visibleRegionDidChange: (() -> Void)? var disableHorizontalTransitionGesture = false var annotationSelected: ((LocationPinAnnotation?) -> Void)? @@ -369,6 +403,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { self.onTouch?() } self.view.addSubview(self.pickerAnnotationContainerView) + self.updateHeadingTransforms() } var isRotateEnabled: Bool = true { @@ -383,6 +418,12 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } } + var isDark: Bool = false { + didSet { + self.mapView?.overrideUserInterfaceStyle = self.isDark ? .dark : .light + } + } + public var trackingMode: LocationTrackingMode = .none { didSet { self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode @@ -391,6 +432,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } else if self.trackingMode != .followWithHeading && self.headingArrowView?.image == nil { self.headingArrowView?.image = generateHeadingArrowImage() } + self.updateHeadingTransforms() } } @@ -460,6 +502,10 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } } + public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) + } + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else { return @@ -478,6 +524,9 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) + self.visibleRegionDidChange?() + let wasDragging = self.isDragging if self.isDragging { self.isDragging = false @@ -543,6 +592,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { container.insertSubview(self.proximityDimView, at: 0) } } + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) } public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { @@ -658,6 +708,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0 + 16.0) self.pickerAnnotationContainerView.addSubview(pickerAnnotationView) self.pickerAnnotationView = pickerAnnotationView + self.updateHeadingTransforms() } else { self.pickerAnnotationView?.removeFromSuperview() self.pickerAnnotationView = nil @@ -696,6 +747,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { self.customUserLocationAnnotationView = annotationView self.pickerAnnotationView?.annotation = annotation + self.updateHeadingTransforms() } else { self.customUserLocationAnnotationView?.removeFromSuperview() self.customUserLocationAnnotationView = nil @@ -705,13 +757,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { public var userHeading: CGFloat? = nil { didSet { - if let heading = self.userHeading { - self.headingArrowView?.isHidden = false - self.headingArrowView?.transform = CGAffineTransform(rotationAngle: CGFloat(heading / 180.0 * CGFloat.pi)) - } else { - self.headingArrowView?.isHidden = true - self.headingArrowView?.transform = CGAffineTransform.identity - } + self.updateHeadingTransforms() } } @@ -869,7 +915,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } if let compassView = self.compassView { - transition.updateFrame(view: compassView, frame: CGRect(origin: CGPoint(x: size.width - compassView.frame.width - 11.0, y: inset + 110.0 + topPadding), size: compassView.frame.size)) + transition.updateFrame(view: compassView, frame: CGRect(origin: CGPoint(x: size.width - compassView.frame.width - 16.0, y: inset + 14.0 + topPadding), size: compassView.frame.size)) } self.applyPendingSetMapCenter() diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift deleted file mode 100644 index 1036a19a41..0000000000 --- a/submodules/LocationUI/Sources/LocationOptionsNode.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import SegmentedControlNode - -public final class LocationOptionsNode: ASDisplayNode { - private var presentationData: PresentationData - - private let backgroundNode: NavigationBackgroundNode - private let separatorNode: ASDisplayNode - private let segmentedControlNode: SegmentedControlNode - - public init(presentationData: PresentationData, hasBackground: Bool = true, updateMapMode: @escaping (LocationMapMode) -> Void) { - self.presentationData = presentationData - - self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: self.presentationData.theme), items: [SegmentedControlItem(title: self.presentationData.strings.Map_Map), SegmentedControlItem(title: self.presentationData.strings.Map_Satellite), SegmentedControlItem(title: self.presentationData.strings.Map_Hybrid)], selectedIndex: 0) - - super.init() - - if hasBackground { - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - self.addSubnode(self.segmentedControlNode) - - self.segmentedControlNode.selectedIndexChanged = { index in - switch index { - case 0: - updateMapMode(.map) - case 1: - updateMapMode(.satellite) - case 2: - updateMapMode(.hybrid) - default: - break - } - } - } - - public func updatePresentationData(_ presentationData: PresentationData) { - self.presentationData = presentationData - self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) - } - - public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundNode.update(size: size, transition: transition) - - transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width - 16.0 - leftInset - rightInset), transition: .immediate) - self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: 0.0), size: controlSize) - } -} diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 634e4c5d3f..09634f18a5 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -30,11 +30,10 @@ class LocationPickerInteraction { let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void let dismissInput: () -> Void - let updateSendActionHighlight: (Bool) -> Void let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -46,7 +45,6 @@ class LocationPickerInteraction { self.updateSearchQuery = updateSearchQuery self.dismissSearch = dismissSearch self.dismissInput = dismissInput - self.updateSendActionHighlight = updateSendActionHighlight self.openHomeWorkInfo = openHomeWorkInfo self.showPlacesInThisArea = showPlacesInThisArea } @@ -104,7 +102,15 @@ public final class LocationPickerController: ViewController, AttachmentContainab case legacy } - public init(context: AccountContext, style: Style = .legacy, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: LocationPickerMode, source: Source = .generic, initialLocation: CLLocationCoordinate2D? = nil, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void) { + public init( + context: AccountContext, + style: Style = .legacy, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + mode: LocationPickerMode, + source: Source = .generic, + initialLocation: CLLocationCoordinate2D? = nil, + completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void + ) { self.context = context self.style = style self.mode = mode @@ -119,11 +125,12 @@ public final class LocationPickerController: ViewController, AttachmentContainab super.init(navigationBarPresentationData: navigationBarPresentationData) self.navigationPresentation = .modal + self._hasGlassStyle = true - if case .glass = style { + if case .share = mode { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } self.updateBarButtons() @@ -179,44 +186,25 @@ public final class LocationPickerController: ViewController, AttachmentContainab guard let strongSelf = self, authorized else { return } - let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription if case let .share(peer, _, _) = strongSelf.mode, let peer = peer, case .user = peer { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } - let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in - controller?.dismissAnimated() - guard let self, let controller else { - return + let sourceView = strongSelf.controllerNode.liveLocationActionSourceView(extend: false) ?? strongSelf.view + let controller = makeLiveLocationDurationContextController( + presentationData: strongSelf.presentationData, + sourceView: sourceView!, + title: title, + selectPeriod: { [weak self] period in + guard let self else { + return + } + self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) + self.dismiss() } - controller.dismissAnimated() - self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) - self.dismiss() - } - - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { - sendLiveLocationImpl(60 * 60 - 1) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { - sendLiveLocationImpl(8 * 60 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { - sendLiveLocationImpl(liveLocationIndefinitePeriod) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in - controller?.dismissAnimated() - }) - ]) - ]) - strongSelf.present(controller, in: .window(.root)) + ) + strongSelf.presentInGlobalOverlay(controller) }) }, sendVenue: { [weak self] venue, queryId, resultId in guard let strongSelf = self else { @@ -316,11 +304,6 @@ public final class LocationPickerController: ViewController, AttachmentContainab } self.searchNavigationContentNode?.deactivate() self.controllerNode.deactivateInput() - }, updateSendActionHighlight: { [weak self] highlighted in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateSendActionHighlight(highlighted) }, openHomeWorkInfo: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index b9a825cc98..5123a26635 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -171,10 +171,9 @@ private enum LocationPickerEntry: Comparable, Identifiable { if let coordinate = coordinate { interaction?.sendLocation(coordinate, name, address?.withUpdated(street: nil)) } - }, highlighted: { highlighted in - interaction?.updateSendActionHighlight(highlighted) + }, highlighted: { _ in }) - case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, isTop): + case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, _): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) @@ -187,10 +186,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { } else if let coordinate { interaction?.sendLocation(coordinate, name, address) } - }, highlighted: { highlighted in - if isTop { - interaction?.updateSendActionHighlight(highlighted) - } + }, highlighted: { _ in }) case let .liveLocation(_, title, subtitle, coordinate): return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: .liveLocation, beginTimeAndTimeout: nil, action: { @@ -361,7 +357,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM private var sendButton: ComponentView? - private let optionsNode: LocationOptionsNode private(set) var searchContainerNode: LocationSearchContainerNode? private var placeholderBackgroundNode: NavigationBackgroundNode? @@ -422,9 +417,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM showPlacesInThisArea: interaction.showPlacesInThisArea ) self.headerNode.mapNode.isRotateEnabled = false - - self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) - + self.shadeNode = ASDisplayNode() self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.shadeNode.alpha = 0.0 @@ -443,7 +436,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM self.addSubnode(self.listNode) self.addSubnode(self.headerNode) - //self.addSubnode(self.optionsNode) + self.listNode.addSubnode(self.emptyResultsTextNode) self.shadeNode.addSubnode(self.innerShadeNode) self.addSubnode(self.shadeNode) @@ -1099,7 +1092,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.headerNode.updatePresentationData(self.presentationData) - self.optionsNode.updatePresentationData(self.presentationData) self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.searchContainerNode?.updatePresentationData(self.presentationData) @@ -1214,6 +1206,16 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM return (self.state.selectedLocation.isCustom || self.state.forceSelection) && !self.state.searchingVenuesAround } + func liveLocationActionSourceView(extend: Bool) -> UIView? { + var result: UIView? + self.listNode.forEachItemNode { itemNode in + if result == nil, let itemNode = itemNode as? LocationActionListItemNode { + result = itemNode.liveLocationContextSourceView(extend: extend) + } + } + return result + } + func requestLayout(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) @@ -1291,12 +1293,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } - let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight - let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight) - transition.updateFrame(node: self.optionsNode, frame: optionsFrame) - self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: insets.left, rightInset: insets.right, transition: transition) - self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions - if let searchContainerNode = self.searchContainerNode { searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition) @@ -1655,11 +1651,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } - func updateSendActionHighlight(_ highlighted: Bool) { - self.headerNode.updateHighlight(highlighted) - self.shadeNode.backgroundColor = highlighted ? self.presentationData.theme.list.itemHighlightedBackgroundColor : self.presentationData.theme.list.plainBackgroundColor - } - func goToUserLocation() { guard let controller = self.controller else { return diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 637782af41..6b74ecc142 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -31,7 +31,7 @@ public class LocationViewParams { } } -class LocationViewInteraction { +final class LocationViewInteraction { let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let toggleTrackingMode: () -> Void @@ -39,12 +39,11 @@ class LocationViewInteraction { let requestDirections: (TelegramMediaMap, String?, OpenInLocationDirections) -> Void let share: () -> Void let setupProximityNotification: (Bool, EngineMessage.Id?) -> Void - let updateSendActionHighlight: (Bool) -> Void let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void let stopLiveLocation: () -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -52,7 +51,6 @@ class LocationViewInteraction { self.requestDirections = requestDirections self.share = share self.setupProximityNotification = setupProximityNotification - self.updateSendActionHighlight = updateSendActionHighlight self.sendLiveLocation = sendLiveLocation self.stopLiveLocation = stopLiveLocation self.present = present @@ -65,10 +63,11 @@ public final class LocationViewController: ViewController { } private let context: AccountContext public var subject: EngineMessage + private var basePresentationData: PresentationData private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var currentMapMode: LocationMapMode = .map private var showAll: Bool - private let isStoryLocation: Bool private let isPreview: Bool private let locationManager = LocationManager() @@ -77,31 +76,36 @@ public final class LocationViewController: ViewController { public var dismissed: () -> Void = {} - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + subject: EngineMessage, + isPreview: Bool = false, + params: LocationViewParams + ) { self.context = context self.subject = subject self.showAll = params.showAll - self.isStoryLocation = isStoryLocation self.isPreview = isPreview - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let initialPresentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + self.basePresentationData = initialPresentationData + self.presentationData = LocationViewController.effectivePresentationData(initialPresentationData, mapMode: .map) super.init(navigationBarPresentationData: nil) self._hasGlassStyle = true + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modal self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in - guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { + guard let strongSelf = self, strongSelf.basePresentationData.theme !== presentationData.theme else { return } - strongSelf.presentationData = presentationData - - if strongSelf.isNodeLoaded { - strongSelf.controllerNode.updatePresentationData(presentationData) - } + strongSelf.basePresentationData = presentationData + strongSelf.updateEffectivePresentationData(animated: true) }) self.interaction = LocationViewInteraction(toggleMapModeSelection: { [weak self] in @@ -117,12 +121,14 @@ public final class LocationViewController: ViewController { guard let strongSelf = self else { return } + strongSelf.currentMapMode = mode strongSelf.controllerNode.updateState { state in var state = state state.mapMode = mode state.displayingMapModeOptions = false return state } + strongSelf.updateEffectivePresentationData(animated: true) }, toggleTrackingMode: { [weak self] in guard let strongSelf = self else { return @@ -174,7 +180,7 @@ public final class LocationViewController: ViewController { } } } else { - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) + strongSelf.push(OpenInOptionsScreen(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl)) } }, share: { [weak self] in guard let strongSelf = self else { @@ -184,7 +190,7 @@ public final class LocationViewController: ViewController { let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { strongSelf.present(context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .mapMedia(location), externalShare: true)), in: .window(.root), with: nil) }) - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) + strongSelf.push(OpenInOptionsScreen(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl)) } }, setupProximityNotification: { [weak self] reset, messageId in guard let strongSelf = self else { @@ -328,11 +334,6 @@ public final class LocationViewController: ViewController { }) }) } - }, updateSendActionHighlight: { [weak self] highlighted in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateSendActionHighlight(highlighted) }, sendLiveLocation: { [weak self] distance, extend, messageId in guard let strongSelf = self else { return @@ -405,7 +406,6 @@ public final class LocationViewController: ViewController { } } |> deliverOnMainQueue).start(next: { peer in - let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title: String if extend { title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription @@ -415,47 +415,32 @@ public final class LocationViewController: ViewController { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } } - - let sendLiveLocationImpl: (Int32) -> Void = { [weak controller] period in - controller?.dismissAnimated() - - if extend { - if let messageId { - let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: period).start() + + let sourceView = strongSelf.controllerNode.liveLocationActionSourceView(extend: extend) ?? strongSelf.view + let controller = makeLiveLocationDurationContextController( + presentationData: strongSelf.presentationData, + sourceView: sourceView!, + title: title, + selectPeriod: { [weak self] period in + guard let strongSelf = self else { + return } - } else { - let _ = (strongSelf.controllerNode.coordinate - |> deliverOnMainQueue).start(next: { coordinate in - params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) - }) - strongSelf.controllerNode.showAll() + if extend { + if let messageId { + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: period).start() + } + } else { + let _ = (strongSelf.controllerNode.coordinate + |> deliverOnMainQueue).start(next: { coordinate in + params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) + }) + + strongSelf.controllerNode.showAll() + } } - } - - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { - sendLiveLocationImpl(15 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { - sendLiveLocationImpl(60 * 60 - 1) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { - sendLiveLocationImpl(8 * 60 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { - sendLiveLocationImpl(liveLocationIndefinitePeriod) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in - controller?.dismissAnimated() - }) - ]) - ]) - strongSelf.present(controller, in: .window(.root)) + ) + strongSelf.presentInGlobalOverlay(controller) }) } }) @@ -482,7 +467,40 @@ public final class LocationViewController: ViewController { deinit { self.presentationDataDisposable?.dispose() } - + + private static func effectivePresentationData(_ presentationData: PresentationData, mapMode: LocationMapMode) -> PresentationData { + switch mapMode { + case .satellite, .hybrid: + if presentationData.theme.overallDarkAppearance { + return presentationData + } + let darkTheme = customizeDefaultDarkPresentationTheme( + theme: defaultDarkPresentationTheme, + editing: false, + title: nil, + accentColor: presentationData.theme.list.itemAccentColor, + backgroundColors: [], + bubbleColors: [], + animateBubbleColors: false, + wallpaper: nil, + baseColor: nil + ) + return presentationData.withUpdated(theme: darkTheme) + case .map: + return presentationData + } + } + + private func updateEffectivePresentationData(animated: Bool) { + let presentationData = LocationViewController.effectivePresentationData(self.basePresentationData, mapMode: self.currentMapMode) + self.presentationData = presentationData + self.statusBar.updateStatusBarStyle(presentationData.theme.rootController.statusBarStyle.style, animated: animated) + + if self.isNodeLoaded { + self.controllerNode.updatePresentationData(presentationData) + } + } + public func goToUserLocation(visibleRadius: Double? = nil) { } @@ -502,7 +520,7 @@ public final class LocationViewController: ViewController { return } - self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview) + self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isPreview: self.isPreview) self.displayNodeDidLoad() self.controllerNode.onAnnotationsReady = { [weak self] in @@ -511,8 +529,6 @@ public final class LocationViewController: ViewController { } strongSelf.controllerNode.showAll() } - - self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -534,4 +550,3 @@ public final class LocationViewController: ViewController { super.dismiss(completion: completion) } } - diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 5f8ec123c2..bb3bcafb7d 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -22,6 +22,8 @@ import GlassControls import BundleIconComponent import EdgeEffect import MultilineTextComponent +import GlassBackgroundComponent +import Weather func getLocation(from message: EngineMessage) -> TelegramMediaMap? { if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap { @@ -57,9 +59,9 @@ public enum LocationViewEntryId: Hashable { } public enum LocationViewEntry: Comparable, Identifiable { - case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) + case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, Bool) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool, EngineMessage.Id?) - case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) + case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, Int) public var stableId: LocationViewEntryId { switch self { @@ -67,15 +69,15 @@ public enum LocationViewEntry: Comparable, Identifiable { return .info case let .toggleLiveLocation(_, _, _, _, _, additional, _): return .toggleLiveLocation(additional) - case let .liveLocation(_, _, _, message, _, _, _, _, _): + case let .liveLocation(_, _, _, message, _, _, _, _): return .liveLocation(message.stableId) } } public static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { - case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): - if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsWalkingTime, lhsHasEta): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { return true } else { return false @@ -86,8 +88,8 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { return false } - case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): - if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { + case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsWalkingTime, lhsIndex): + if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { return true } else { return false @@ -113,11 +115,11 @@ public enum LocationViewEntry: Comparable, Identifiable { case .liveLocation: return true } - case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, lhsIndex): switch rhs { case .info, .toggleLiveLocation: return false - case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, rhsIndex): return lhsIndex < rhsIndex } } @@ -125,7 +127,7 @@ public enum LocationViewEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { switch self { - case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): + case let .info(_, location, address, distance, drivingTime, walkingTime, hasEta): let addressString: String? if let address = address { addressString = address @@ -138,12 +140,10 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { distanceString = nil } - return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, walkingTime: walkingTime, hasEta: hasEta, action: { interaction?.goToCoordinate(location.coordinate) }, drivingAction: { interaction?.requestDirections(location, nil, .driving) - }, transitAction: { - interaction?.requestDirections(location, nil, .transit) }, walkingAction: { interaction?.requestDirections(location, nil, .walking) }) @@ -164,7 +164,7 @@ public enum LocationViewEntry: Comparable, Identifiable { icon = .liveLocation } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, isOpaque: false, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { if beginTimeAndTimeout != nil { if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { if additional { @@ -178,15 +178,14 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { interaction?.sendLiveLocation(nil, false, nil) } - }, highlighted: { highlight in - interaction?.updateSendActionHighlight(highlight) + }, highlighted: { _ in }) - case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): + case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, walkingTime, _): var title: String? if let author = message.author { title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) } - return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { + return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, walkingTime: walkingTime, action: { if let location = getLocation(from: message) { interaction?.goToCoordinate(location.coordinate) } @@ -194,10 +193,6 @@ public enum LocationViewEntry: Comparable, Identifiable { if let location = getLocation(from: message) { interaction?.requestDirections(location, title, .driving) } - }, transitAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .transit) - } }, walkingAction: { if let location = getLocation(from: message) { interaction?.requestDirections(location, title, .walking) @@ -285,7 +280,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private var subject: EngineMessage private let interaction: LocationViewInteraction private let locationManager: LocationManager - private let isStoryLocation: Bool private let isPreview: Bool private var rightBarButtonAction: LocationViewRightBarButton = .none @@ -295,11 +289,13 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private let title = ComponentView() private let listNode: ListView + let backgroundView = GlassBackgroundView() let headerNode: LocationMapHeaderNode private var enqueuedTransitions: [LocationViewTransaction] = [] private var disposable: Disposable? + private let weatherDisposable = MetaDisposable() private var state: LocationViewState private let statePromise: Promise @@ -312,14 +308,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var onAnnotationsReady: (() -> Void)? private let travelDisposables = DisposableSet() - private var travelTimes: [EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)] = [:] { + private var travelTimes: [EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime)] = [:] { didSet { self.travelTimesPromise.set(.single(self.travelTimes)) } } - private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:]) + private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime)]>([:]) - init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) { + init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isPreview: Bool) { self.context = context self.controller = controller self.presentationData = presentationData @@ -327,14 +323,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.subject = subject self.interaction = interaction self.locationManager = locationManager - self.isStoryLocation = isStoryLocation self.isPreview = isPreview self.state = LocationViewState() self.statePromise = Promise(self.state) self.listNode = ListViewImpl() - self.listNode.backgroundColor = .clear //self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear + self.listNode.limitHitTestToNodes = true self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.accessibilityPageScrolledString = { row, count in @@ -342,6 +338,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } var setupProximityNotificationImpl: ((Bool) -> Void)? + var weatherPressedImpl: (() -> Void)? self.headerNode = LocationMapHeaderNode( presentationData: presentationData, glass: true, @@ -351,6 +348,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan goToUserLocation: interaction.toggleTrackingMode, setupProximityNotification: { reset in setupProximityNotificationImpl?(reset) + }, + weatherPressed: { + weatherPressedImpl?() } ) @@ -358,35 +358,42 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.addSubnode(self.headerNode) if !self.isPreview { self.addSubnode(self.listNode) } - self.addSubnode(self.headerNode) let userLocation: Signal = .single(nil) |> then( throttledUserLocation(self.headerNode.mapNode.userLocation) ) - var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating)) var address: Signal = .single(nil) + let subjectLocation = getLocation(from: subject) + let isStaticLocationView: Bool + if let subjectLocation { + isStaticLocationView = subjectLocation.liveBroadcastingTimeout == nil + } else { + isStaticLocationView = false + } + let locale = localeWithStrings(presentationData.strings) - if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { - eta = .single((.calculating, .calculating, .calculating)) - |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + if let location = subjectLocation, isStaticLocationView { + self.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: LocationMapNode.viewMapSpan, animated: false) + + eta = .single((.calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) }) if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { @@ -402,13 +409,21 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } } - let liveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) + let actualLiveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) |> map { _, messages -> [EngineMessage] in return messages } + let renderLiveLocations: Signal<[EngineMessage], NoError> + if isStaticLocationView { + renderLiveLocations = .single([]) + |> then(actualLiveLocations) + } else { + renderLiveLocations = actualLiveLocations + } + setupProximityNotificationImpl = { reset in - let _ = (liveLocations + let _ = (actualLiveLocations |> take(1) |> deliverOnMainQueue).start(next: { messages in var ownMessageId: EngineMessage.Id? @@ -424,15 +439,36 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan }) } + weatherPressedImpl = { + if let location = subjectLocation { + context.sharedContext.openExternalUrl( + context: context, + urlContext: .generic, + url: "https://weather.apple.com/?lat=\(location.latitude)&long=\(location.longitude)", + forceExternal: true, + presentationData: presentationData, + navigationController: nil, + dismissInput: {} + ) + } + } + let previousState = Atomic(value: nil) let previousUserAnnotation = Atomic(value: nil) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) let previousEntries = Atomic<[LocationViewEntry]?>(value: nil) let previousHadTravelTimes = Atomic(value: false) - let selfPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let actualSelfPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let renderSelfPeer: Signal + if isStaticLocationView { + renderSelfPeer = .single(nil) + |> then(actualSelfPeer) + } else { + renderSelfPeer = actualSelfPeer + } - self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get()) + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), renderSelfPeer, renderLiveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta, travelTimes in if let strongSelf = self, let location = getLocation(from: subject) { var entries: [LocationViewEntry] = [] @@ -453,7 +489,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } - entries.append(.info(presentationData.theme, location, address, distance, eta.0, eta.1, eta.2, !isStoryLocation)) + entries.append(.info(presentationData.theme, location, address, distance, eta.0, eta.1, true)) annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: location, queryId: nil, resultId: nil, forcedSelection: true)) } else { @@ -529,7 +565,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan for message in effectiveLiveLocations { if let location = getLocation(from: message) { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, message.threadId != nil { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, let threadId = message.threadId, threadId != 1 { continue } @@ -555,60 +591,57 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading) } else { var drivingTime: ExpectedTravelTime = .unknown - var transitTime: ExpectedTravelTime = .unknown var walkingTime: ExpectedTravelTime = .unknown if !isLocationView && message.author?.id != context.account.peerId { - let signal = combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + let signal = combineLatest( + queue: Queue.mainQueue(), + getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), + getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking) + ) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) } - if let (previousTimestamp, maybeDrivingTime, maybeTransitTime, maybeWalkingTime) = travelTimes[message.id] { + if let (previousTimestamp, maybeDrivingTime, maybeWalkingTime) = travelTimes[message.id] { drivingTime = maybeDrivingTime - transitTime = maybeTransitTime walkingTime = maybeWalkingTime if timestamp > previousTimestamp + 60.0 { - strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, transitTime, walkingTime in + strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, walkingTime in guard let strongSelf = self else { return } let timestamp = CACurrentMediaTime() var travelTimes = strongSelf.travelTimes - travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + travelTimes[message.id] = (timestamp, drivingTime, walkingTime) strongSelf.travelTimes = travelTimes })) } } else { drivingTime = .calculating - transitTime = .calculating walkingTime = .calculating - strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, transitTime, walkingTime in + strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, walkingTime in guard let strongSelf = self else { return } let timestamp = CACurrentMediaTime() var travelTimes = strongSelf.travelTimes - travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + travelTimes[message.id] = (timestamp, drivingTime, walkingTime) strongSelf.travelTimes = travelTimes })) } } annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: message.author?.id == context.account.peerId, heading: location.heading)) - entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, index)) + entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, walkingTime, index)) } index += 1 } @@ -738,14 +771,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if !isPreview { self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in - guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { + guard let self, self.listNode.scrollEnabled else { return } - let overlap: CGFloat = 0.0 - strongSelf.listOffset = max(0.0, offset) - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) - listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) - strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + self.listOffset = max(0.0, offset) + self.updateHeader(transition: listTransition) } } @@ -790,14 +820,25 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.locationManager.manager.startUpdatingHeading() self.locationManager.manager.delegate = self + + if !self.isPreview, let location = getLocation(from: subject) { + self.requestWeatherData(coordinate: location.coordinate) + } } deinit { self.disposable?.dispose() + self.weatherDisposable.dispose() self.travelDisposables.dispose() self.locationManager.manager.stopUpdatingHeading() } + override func didLoad() { + super.didLoad() + + self.view.insertSubview(self.backgroundView, aboveSubview: self.headerNode.view) + } + func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { if newHeading.headingAccuracy < 0.0 { self.headerNode.mapNode.userHeading = nil @@ -814,17 +855,70 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.presentationDataPromise.set(.single(presentationData)) self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear self.headerNode.updatePresentationData(self.presentationData) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate) + } } func updateState(_ f: (LocationViewState) -> LocationViewState) { self.state = f(self.state) self.statePromise.set(.single(self.state)) } - - func updateSendActionHighlight(_ highlighted: Bool) { - self.headerNode.updateHighlight(highlighted) + + private func requestWeatherData(coordinate: CLLocationCoordinate2D) { + self.weatherDisposable.set((Weather.requestWeatherData(context: self.context, location: coordinate) + |> deliverOnMainQueue).start(next: { [weak self] weatherData in + guard let self else { + return + } + if let weatherData { + self.headerNode.updateWeatherData(context: self.context, emoji: weatherData.emoji, temperature: stringForTemperature(weatherData.temperature), animated: true) + } else { + self.headerNode.clearWeatherData(animated: true) + } + })) + } + + func updateHeader(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationBarHeight) = self.validLayout else { + return + } + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) + transition.updateFrame(node: self.headerNode, frame: headerFrame) + + let headerHeight: CGFloat + if self.isPreview { + headerHeight = layout.size.height + } else if let listOffset = self.listOffset { + headerHeight = max(0.0, listOffset) + } else { + headerHeight = headerFrame.height + } + let headerSize = CGSize(width: headerFrame.width, height: headerHeight) + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 6.0, offset: 0.0, size: headerSize, transition: transition) + + let backgroundHeight = layout.size.height - headerHeight + + let glassInset: CGFloat = 6.0 + let backgroundSize = CGSize(width: layout.size.width - glassInset * 2.0, height: backgroundHeight) + + let bottomCornerRadius = max(24.0, layout.deviceMetrics.screenCornerRadius) - 2.0 + + self.backgroundView.update( + size: backgroundSize, + cornerRadii: .init( + topLeft: 38.0, + topRight: 38.0, + bottomLeft: bottomCornerRadius, + bottomRight: bottomCornerRadius + ), + isDark: self.presentationData.theme.overallDarkAppearance, + tintColor: .init(kind: .panel), + transition: ComponentTransition(transition) + ) + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: layout.size.height - backgroundSize.height - glassInset), size: backgroundSize)) } private func enqueueTransition(_ transition: LocationViewTransaction) { @@ -927,6 +1021,16 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.headerNode.mapNode.showAll() } + func liveLocationActionSourceView(extend: Bool) -> UIView? { + var result: UIView? + self.listNode.forEachItemNode { itemNode in + if result == nil, let itemNode = itemNode as? LocationActionListItemNode { + result = itemNode.liveLocationContextSourceView(extend: extend) + } + } + return result + } + private func displayProximityAlertTooltip() { guard let location = self.headerNode.proximityButtonFrame().flatMap({ frame -> CGRect in return self.headerNode.view.convert(frame, to: nil) @@ -962,7 +1066,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) - let optionsHeight: CGFloat = 38.0 var actionHeight: CGFloat? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? LocationActionListItemNode { @@ -974,25 +1077,16 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let overlap: CGFloat = 0.0 var topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - overlap - if !self.isStoryLocation { - topInset -= 100.0 - } + topInset -= 100.0 + if let location = getLocation(from: self.subject), location.liveBroadcastingTimeout != nil { topInset += 66.0 } - let headerHeight: CGFloat - if self.isPreview { - headerHeight = layout.size.height - } else if let listOffset = self.listOffset { - headerHeight = max(0.0, listOffset + overlap) - } else { - headerHeight = topInset + overlap + if self.listOffset == nil { + self.listOffset = topInset } - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) - transition.updateFrame(node: self.headerNode, frame: headerFrame) - - self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: CGSize(width: headerFrame.width, height: headerHeight), transition: transition) + self.updateHeader(transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -1005,7 +1099,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if !self.isPreview { let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 80.0)) transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) - self.topEdgeEffectView.update(content: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.topEdgeEffectView.update(content: !self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) if self.topEdgeEffectView.superview == nil { self.view.addSubview(self.topEdgeEffectView) } diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 8088d37de4..eb9d00e7bf 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -36,7 +36,6 @@ swift_library( "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/ChatMessageBackground:ChatMessageBackground", "//submodules/SparseItemGrid:SparseItemGrid", "//submodules/UndoUI:UndoUI", @@ -57,6 +56,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/Components/BundleIconComponent", diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 254707296d..7d939ef00c 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -152,6 +152,15 @@ func presentLegacyMediaPickerGallery( legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } @@ -165,6 +174,9 @@ func presentLegacyMediaPickerGallery( livePhotoButton.present = present return livePhotoButton } + paintStickersContext.photoToolbarView = { backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) + } paintStickersContext.editCover = { dimensions, completion in editCover(dimensions, completion) } @@ -298,7 +310,7 @@ func presentLegacyMediaPickerGallery( } } if !isScheduledMessages && peer != nil { - model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in + model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model, weak paintStickersContext] item, sourceView in if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext { var effectiveHasSchedule = hasSchedule @@ -344,37 +356,35 @@ func presentLegacyMediaPickerGallery( let _ = (sendWhenOnlineAvailable |> take(1) |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in - let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) let dismissImpl = { [weak model] in model?.dismiss(true, false) dismissAll() } - sheetController.send = { + let send = { completed(item.asset, false, nil, { dismissImpl() }) } - sheetController.sendSilently = { [weak model] in + let sendSilently = { [weak model] in model?.interfaceView.onDismiss() completed(item.asset, true, nil, { dismissImpl() }) } - sheetController.sendWhenOnline = { + let sendWhenOnline = { completed(item.asset, false, scheduleWhenOnlineTimestamp, { dismissImpl() }) } - sheetController.schedule = { + let schedule = { presentSchedulePicker(true, { time, silentPosting in completed(item.asset, silentPosting, time, { dismissImpl() }) }) } - sheetController.sendWithTimer = { + let sendWithTimer = { presentTimerPicker { time in var items = selectionContext.selectedItems() ?? [] items.append(item.asset as Any) @@ -388,6 +398,20 @@ func presentLegacyMediaPickerGallery( }) } } + + if let sourceView, let paintStickersContext, paintStickersContext.presentMediaPickerSendActionMenu?(sourceView, hasSilentPosting, sendWhenOnlineAvailable && effectiveHasSchedule, effectiveHasSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer) == true { + let hapticFeedback = HapticFeedback() + hapticFeedback.impact() + return + } + + let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) + sheetController.send = send + sheetController.sendSilently = sendSilently + sheetController.sendWhenOnline = sendWhenOnline + sheetController.schedule = schedule + sheetController.sendWithTimer = sendWithTimer sheetController.customDismissBlock = { [weak legacySheetController] in legacySheetController?.dismiss() } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift index e2b5f31a29..8f2d4df733 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift @@ -167,6 +167,7 @@ final class MediaGroupsContextMenuContent: ContextControllerItemsContent { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift index 79dbd0c8c9..8171f784fc 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift @@ -470,7 +470,7 @@ public final class MediaGroupsScreen: ViewController, AttachmentContainable { self.updateNavigationStack { current in var mediaPickerContext: AttachmentMediaPickerContext? if let first = current.first as? MediaPickerScreenImpl { - mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext + mediaPickerContext = first.mediaPickerContext } return (current.filter { $0 !== self }, mediaPickerContext) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift index c48fc25276..a26d7c686a 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift @@ -15,7 +15,7 @@ final class MediaPickerManageNode: ASDisplayNode { private let measureButtonNode: ImmediateTextNode private let buttonNode: SolidRoundedButtonNode - var pressed: () -> Void = {} + var pressed: (UIView) -> Void = { _ in } override init() { self.textNode = ImmediateTextNode() @@ -33,7 +33,10 @@ final class MediaPickerManageNode: ASDisplayNode { self.addSubnode(self.buttonNode) self.buttonNode.pressed = { [weak self] in - self?.pressed() + guard let self else { + return + } + self.pressed(self.buttonNode.view) } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift b/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift new file mode 100644 index 0000000000..b7ae2d96a4 --- /dev/null +++ b/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift @@ -0,0 +1,1187 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import AccountContext +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent +import GlassBarButtonComponent +import PlainButtonComponent +import BundleIconComponent + +private let toolbarButtonSide: CGFloat = 44.0 +private let toolbarSideButtonSide: CGFloat = 44.0 +private let centerButtonSpacing: CGFloat = 10.0 + +private let toolbarTabOrder: [TGPhotoEditorTab] = [ + .cropTab, + .stickerTab, + .paintTab, + .eraserTab, + .textTab, + .toolsTab, + .rotateTab, + .qualityTab, + .timerTab, + .mirrorTab, + .aspectRatioTab, + .tintTab, + .blurTab, + .curvesTab +] + +private let dontHighlightOnSelectionTabs = Set([ + TGPhotoEditorTab.rotateTab.rawValue, + TGPhotoEditorTab.stickerTab.rawValue, + TGPhotoEditorTab.textTab.rawValue, + TGPhotoEditorTab.qualityTab.rawValue, + TGPhotoEditorTab.timerTab.rawValue, + TGPhotoEditorTab.mirrorTab.rawValue, + TGPhotoEditorTab.aspectRatioTab.rawValue +]) + + +private final class MediaPickerPhotoToolbarImageCache { + private var images: [String: UIImage] = [:] + + func qualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? { + let key = "quality-\(isPhoto)-\(highQuality)-\(preset)-\(self.colorKey(color))" + if let image = self.images[key] { + return image + } + let image = generateQualityIcon(isPhoto: isPhoto, highQuality: highQuality, preset: preset, color: color) + self.images[key] = image + return image + } + + func timerIcon(value: Int, color: UIColor) -> UIImage? { + let key = "timer-\(value)-\(self.colorKey(color))" + if let image = self.images[key] { + return image + } + let image = generateTimerIcon(value: value, color: color) + self.images[key] = image + return image + } + + private func colorKey(_ color: UIColor) -> String { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + return "\(Int(red * 255.0))-\(Int(green * 255.0))-\(Int(blue * 255.0))-\(Int(alpha * 255.0))" + } + var white: CGFloat = 0.0 + if color.getWhite(&white, alpha: &alpha) { + return "\(Int(white * 255.0))-\(Int(alpha * 255.0))" + } + return color.description + } +} + +private func generateQualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? { + let label: String + if isPhoto { + label = highQuality ? "HD" : "SD" + } else { + switch preset { + case 1: + label = "240" + case 2: + label = "360" + case 3: + label = "480" + case 4: + label = "720" + case 5: + label = "HD" + default: + label = "480" + } + } + + let size = CGSize(width: isPhoto ? 24.0 : 28.0, height: 24.0) + let lineWidth = 2.0 - UIScreenPixel + let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0) + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return nil + } + + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 7.0).cgPath) + context.strokePath() + + let font = Font.with(size: 11.0, design: .round, weight: .bold) + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: color, + .kern: -0.7 + ] + let textSize = label.size(withAttributes: attributes) + label.draw( + in: CGRect( + x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, + y: 5.0, + width: textSize.width, + height: textSize.height + ), + withAttributes: attributes + ) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private func generateTimerIcon(value: Int, color: UIColor) -> UIImage? { + let size = CGSize(width: 24.0, height: 24.0) + let lineWidth = 2.0 - UIScreenPixel + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return nil + } + + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + let bodyRect = CGRect(x: 4.0, y: 5.0, width: 16.0, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0) + context.strokeEllipse(in: bodyRect) + + context.move(to: CGPoint(x: 9.0, y: 2.5)) + context.addLine(to: CGPoint(x: 15.0, y: 2.5)) + context.strokePath() + + context.move(to: CGPoint(x: 12.0, y: 5.0)) + context.addLine(to: CGPoint(x: 12.0, y: 8.0)) + context.strokePath() + + if value == 0 { + context.move(to: CGPoint(x: 12.0, y: 13.0)) + context.addLine(to: CGPoint(x: 12.0, y: 9.0)) + context.move(to: CGPoint(x: 12.0, y: 13.0)) + context.addLine(to: CGPoint(x: 15.0, y: 13.0)) + context.strokePath() + } else { + let label = "\(value)" + let font = Font.with(size: 10.0, design: .round, weight: .semibold) + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: color + ] + let textSize = label.size(withAttributes: attributes) + label.draw( + in: CGRect( + x: floorToScreenPixels((size.width - textSize.width) / 2.0), + y: 9.0, + width: textSize.width, + height: textSize.height + ), + withAttributes: attributes + ) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private final class MediaPickerPhotoToolbarComponent: Component { + let context: AccountContext + let solidBackground: Bool + let backButtonType: TGPhotoEditorBackButton + let doneButtonType: TGPhotoEditorDoneButton + let currentTabs: TGPhotoEditorTab + let activeTab: TGPhotoEditorTab + let highlightedTabs: TGPhotoEditorTab + let disabledTabs: TGPhotoEditorTab + let qualityIsPhoto: Bool + let qualityHighQuality: Bool + let qualityPreset: Int + let timerValue: Int + let hasSendStarsButton: Bool + let sendPaidMessageStars: Int64 + let editButtonsHidden: Bool + let editButtonsEnabled: Bool + let centerButtonsHidden: Bool + let allButtonsHidden: Bool + let cancelDoneButtonsHidden: Bool + let doneButtonEnabled: Bool + let interfaceOrientation: UIInterfaceOrientation + let bottomInset: CGFloat + let infoString: String? + let cancelPressed: (() -> Void)? + let donePressed: (() -> Void)? + let doneLongPressed: ((Any?) -> Void)? + let tabPressed: ((TGPhotoEditorTab) -> Void)? + + init( + context: AccountContext, + solidBackground: Bool, + backButtonType: TGPhotoEditorBackButton, + doneButtonType: TGPhotoEditorDoneButton, + currentTabs: TGPhotoEditorTab, + activeTab: TGPhotoEditorTab, + highlightedTabs: TGPhotoEditorTab, + disabledTabs: TGPhotoEditorTab, + qualityIsPhoto: Bool, + qualityHighQuality: Bool, + qualityPreset: Int, + timerValue: Int, + hasSendStarsButton: Bool, + sendPaidMessageStars: Int64, + editButtonsHidden: Bool, + editButtonsEnabled: Bool, + centerButtonsHidden: Bool, + allButtonsHidden: Bool, + cancelDoneButtonsHidden: Bool, + doneButtonEnabled: Bool, + interfaceOrientation: UIInterfaceOrientation, + bottomInset: CGFloat, + infoString: String?, + cancelPressed: (() -> Void)?, + donePressed: (() -> Void)?, + doneLongPressed: ((Any?) -> Void)?, + tabPressed: ((TGPhotoEditorTab) -> Void)? + ) { + self.context = context + self.solidBackground = solidBackground + self.backButtonType = backButtonType + self.doneButtonType = doneButtonType + self.currentTabs = currentTabs + self.activeTab = activeTab + self.highlightedTabs = highlightedTabs + self.disabledTabs = disabledTabs + self.qualityIsPhoto = qualityIsPhoto + self.qualityHighQuality = qualityHighQuality + self.qualityPreset = qualityPreset + self.timerValue = timerValue + self.hasSendStarsButton = hasSendStarsButton + self.sendPaidMessageStars = sendPaidMessageStars + self.editButtonsHidden = editButtonsHidden + self.editButtonsEnabled = editButtonsEnabled + self.centerButtonsHidden = centerButtonsHidden + self.allButtonsHidden = allButtonsHidden + self.cancelDoneButtonsHidden = cancelDoneButtonsHidden + self.doneButtonEnabled = doneButtonEnabled + self.interfaceOrientation = interfaceOrientation + self.bottomInset = bottomInset + self.infoString = infoString + self.cancelPressed = cancelPressed + self.donePressed = donePressed + self.doneLongPressed = doneLongPressed + self.tabPressed = tabPressed + } + + static func ==(lhs: MediaPickerPhotoToolbarComponent, rhs: MediaPickerPhotoToolbarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.solidBackground != rhs.solidBackground { + return false + } + if lhs.backButtonType != rhs.backButtonType { + return false + } + if lhs.doneButtonType != rhs.doneButtonType { + return false + } + if lhs.currentTabs != rhs.currentTabs { + return false + } + if lhs.activeTab != rhs.activeTab { + return false + } + if lhs.highlightedTabs != rhs.highlightedTabs { + return false + } + if lhs.disabledTabs != rhs.disabledTabs { + return false + } + if lhs.qualityIsPhoto != rhs.qualityIsPhoto { + return false + } + if lhs.qualityHighQuality != rhs.qualityHighQuality { + return false + } + if lhs.qualityPreset != rhs.qualityPreset { + return false + } + if lhs.timerValue != rhs.timerValue { + return false + } + if lhs.hasSendStarsButton != rhs.hasSendStarsButton { + return false + } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } + if lhs.editButtonsHidden != rhs.editButtonsHidden { + return false + } + if lhs.editButtonsEnabled != rhs.editButtonsEnabled { + return false + } + if lhs.centerButtonsHidden != rhs.centerButtonsHidden { + return false + } + if lhs.allButtonsHidden != rhs.allButtonsHidden { + return false + } + if lhs.cancelDoneButtonsHidden != rhs.cancelDoneButtonsHidden { + return false + } + if lhs.doneButtonEnabled != rhs.doneButtonEnabled { + return false + } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.infoString != rhs.infoString { + return false + } + return true + } + + final class View: UIView { + fileprivate let cancelButton = ComponentView() + fileprivate let doneButton = ComponentView() + private let doneButtonContextView = ContextControllerSourceView() + + private let buttonsBackgroundView = GlassBackgroundView() + private let selectionView = UIView() + private let infoLabel = UILabel() + private let imageCache = MediaPickerPhotoToolbarImageCache() + private var centerButtonViews: [UInt: ComponentView] = [:] + + private var component: MediaPickerPhotoToolbarComponent? + private var cancelPressed: (() -> Void)? + private var donePressed: (() -> Void)? + private var doneLongPressed: ((Any?) -> Void)? + private var tabPressed: ((TGPhotoEditorTab) -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = false + self.addSubview(self.buttonsBackgroundView) + + self.selectionView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.15) + self.selectionView.isUserInteractionEnabled = false + self.selectionView.alpha = 0.0 + + self.infoLabel.backgroundColor = .clear + self.infoLabel.textAlignment = .center + self.infoLabel.textColor = .white + self.infoLabel.font = Font.regular(13.0) + self.infoLabel.isUserInteractionEnabled = false + self.addSubview(self.infoLabel) + + self.doneButtonContextView.beginDelay = 0.4 + self.doneButtonContextView.activated = { [weak self] gesture, _ in + guard let self else { + gesture.cancel() + return + } + self.doneLongPressed?(self.doneButtonContextView) + } + self.doneButtonContextView.shouldBegin = { [weak self] _ in + guard let self, let component = self.component else { + return false + } + return self.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden + } + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + fileprivate var cancelButtonFrame: CGRect { + return self.cancelButton.view?.frame ?? .zero + } + + fileprivate var doneButtonFrame: CGRect { + return self.doneButtonContextView.frame + } + + fileprivate var doneButtonSourceView: UIView { + return self.doneButtonContextView + } + + fileprivate func viewForTab(_ tab: TGPhotoEditorTab) -> UIView? { + return self.centerButtonViews[tab.rawValue]?.view + } + + func update(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.cancelPressed = component.cancelPressed + self.donePressed = component.donePressed + self.doneLongPressed = component.doneLongPressed + self.tabPressed = component.tabPressed + + //let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let accentColor = UIColor(rgb: 0xffd300) //presentationData.theme.list.itemAccentColor + let selectedIconColor = UIColor.white //(rgb: 0xffd300) + + self.backgroundColor = .clear //component.solidBackground ? UIColor(rgb: 0x000000, alpha: 0.55) : .clear + + let cancelSize = self.cancelButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: toolbarButtonSide, height: toolbarButtonSide), + backgroundColor: nil, + isDark: true, + state: .glass, + isEnabled: !component.cancelDoneButtonsHidden && !component.allButtonsHidden, + isVisible: true, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent( + name: component.backButtonType == TGPhotoEditorBackButtonBack ? "Navigation/Back" : "Navigation/Close", + tintColor: .white + ) + ) + ), + action: { [weak self] _ in + self?.cancelPressed?() + } + ) + ), + environment: {}, + containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide) + ) + + let doneIconName: String + let doneBackgroundColor: UIColor? = UIColor(rgb: 0x0088ff) + var doneWidth: CGFloat = toolbarButtonSide + switch component.doneButtonType { + case TGPhotoEditorDoneButtonSend: + doneIconName = "Chat/Input/Text/SendIcon" + doneWidth = 46.0 + case TGPhotoEditorDoneButtonSchedule: + doneIconName = "Chat/Input/ScheduleIcon" + doneWidth = 46.0 + case TGPhotoEditorDoneButtonDone: + doneIconName = "Navigation/Done" + default: + doneIconName = "Navigation/Done" + } + + let doneSize = self.doneButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: doneWidth, height: toolbarButtonSide), + backgroundColor: doneBackgroundColor, + isDark: true, + state: doneBackgroundColor != nil ? .tintedGlass : .glass, + isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden, + isVisible: true, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + BundleIconComponent( + name: doneIconName, + tintColor: .white + ) + ) + ), + action: { [weak self] _ in + self?.donePressed?() + } + ) + ), + environment: {}, + containerSize: CGSize(width: doneWidth, height: toolbarButtonSide) + ) + +// let doneContent: AnyComponentWithIdentity +// let doneFixedSize: CGSize? +// let doneAvailableSize: CGSize +// if component.hasSendStarsButton { +// let text = "\u{2b50}\u{fe0f} \(component.sendPaidMessageStars)" +// doneContent = AnyComponentWithIdentity( +// id: "stars-\(component.sendPaidMessageStars)", +// component: AnyComponent(Text(text: text, font: Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers), color: .white)) +// ) +// doneFixedSize = nil +// doneAvailableSize = CGSize(width: 180.0, height: toolbarSideButtonSide) +// } else { +// doneContent = self.sideButtonContent(iconName: self.doneIconName(component.doneButtonType), color: .white, id: "done-\(component.doneButtonType.rawValue)") +// doneFixedSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide) +// doneAvailableSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide) +// } +// let doneSize = self.doneButton.update( +// content: doneContent, +// fixedSize: doneFixedSize, +// availableSize: doneAvailableSize, +// isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden, +// transition: transition +// ) + + let sideAlpha = component.allButtonsHidden ? 0.0 : 1.0 + let sideFrames = self.sideButtonFrames(availableSize: availableSize, cancelSize: cancelSize, doneSize: doneSize, component: component) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setAlpha(view: cancelButtonView, alpha: sideAlpha) + transition.setFrame(view: cancelButtonView, frame: sideFrames.cancel) + } + if let doneButtonView = self.doneButton.view { + if self.doneButtonContextView.superview == nil { + self.addSubview(self.doneButtonContextView) + } + if doneButtonView.superview !== self.doneButtonContextView { + self.doneButtonContextView.addSubview(doneButtonView) + } + self.doneButtonContextView.targetViewForActivationProgress = doneButtonView + self.doneButtonContextView.isGestureEnabled = component.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden + transition.setAlpha(view: self.doneButtonContextView, alpha: sideAlpha * (component.doneButtonEnabled ? 1.0 : 0.2)) + transition.setFrame(view: self.doneButtonContextView, frame: sideFrames.done) + transition.setFrame(view: doneButtonView, frame: CGRect(origin: .zero, size: sideFrames.done.size)) + } + + self.updateCenterButtons( + component: component, + availableSize: availableSize, + doneSize: doneSize, + selectedIconColor: selectedIconColor, + accentColor: accentColor, + transition: transition + ) + + self.updateInfoLabel(component: component, availableSize: availableSize, transition: transition) + + return availableSize + } + + private func doneIconName(_ type: TGPhotoEditorDoneButton) -> String { + switch type.rawValue { + case 1: + return "Editor/Commit" + case 2: + return "Editor/Commit" + case 3: + return "PhotoPickerSendIcon" + default: + return "PhotoPickerSendIcon" + } + } + + private func sideButtonFrames(availableSize: CGSize, cancelSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> (cancel: CGRect, done: CGRect) { + let sideInset: CGFloat = 26.0 + if availableSize.width > availableSize.height { + let cancelFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: cancelSize) + let doneFrame: CGRect + if component.hasSendStarsButton { + doneFrame = CGRect(x: availableSize.width - doneSize.width - 2.0, y: 0.0, width: doneSize.width, height: doneSize.height) + } else { + doneFrame = CGRect(x: availableSize.width - doneSize.width - sideInset, y: 0.0, width: doneSize.width, height: doneSize.height) + } + return (cancelFrame, doneFrame) + } else { + let offset: CGFloat = component.interfaceOrientation == .landscapeLeft ? availableSize.width - toolbarSideButtonSide : 0.0 + let cancelFrame = CGRect(x: offset, y: availableSize.height - toolbarSideButtonSide, width: toolbarSideButtonSide, height: toolbarSideButtonSide) + let doneFrame = CGRect(x: offset, y: 0.0, width: toolbarSideButtonSide, height: toolbarSideButtonSide) + return (cancelFrame, doneFrame) + } + } + + private func updateCenterButtons(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, doneSize: CGSize, selectedIconColor: UIColor, accentColor: UIColor, transition: ComponentTransition) { + let tabs = toolbarTabOrder.filter { component.currentTabs.contains($0) } + let visibleRawValues = Set(tabs.map { $0.rawValue }) + + for (rawValue, buttonView) in Array(self.centerButtonViews) { + if !visibleRawValues.contains(rawValue) { + if let view = buttonView.view { + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + self.centerButtonViews.removeValue(forKey: rawValue) + } + } + + guard !tabs.isEmpty else { + transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0) + transition.setAlpha(view: self.selectionView, alpha: 0.0) + self.buttonsBackgroundView.isUserInteractionEnabled = false + return + } + + let buttonFrames = self.centerButtonFrames(tabs: tabs, availableSize: availableSize, doneSize: doneSize, component: component) + var backgroundFrame = CGRect.null + for tab in tabs { + if let frame = buttonFrames[tab.rawValue] { + backgroundFrame = backgroundFrame.union(frame) + } + } + if backgroundFrame.isNull { + transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0) + transition.setAlpha(view: self.selectionView, alpha: 0.0) + self.buttonsBackgroundView.isUserInteractionEnabled = false + return + } + backgroundFrame = CGRect( + x: floorToScreenPixels(backgroundFrame.minX), + y: floorToScreenPixels(backgroundFrame.minY), + width: ceil(backgroundFrame.width), + height: ceil(backgroundFrame.height) + ) + + let centerHidden = component.allButtonsHidden || component.centerButtonsHidden || component.editButtonsHidden + let centerAlpha: CGFloat = centerHidden ? 0.0 : (component.editButtonsEnabled ? 1.0 : 0.2) + self.buttonsBackgroundView.isUserInteractionEnabled = !centerHidden && component.editButtonsEnabled + transition.setAlpha(view: self.buttonsBackgroundView, alpha: centerAlpha) + transition.setFrame(view: self.buttonsBackgroundView, frame: backgroundFrame) + + let minSide = min(backgroundFrame.width, backgroundFrame.height) + self.buttonsBackgroundView.update(size: backgroundFrame.size, cornerRadius: minSide * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !centerHidden, transition: transition) + + if self.selectionView.superview == nil { + self.buttonsBackgroundView.contentView.insertSubview(self.selectionView, at: 0) + } else { + self.buttonsBackgroundView.contentView.sendSubviewToBack(self.selectionView) + } + + var selectionFrame: CGRect? + + for tab in tabs { + guard let buttonFrame = buttonFrames[tab.rawValue] else { + continue + } + + let rawValue = tab.rawValue + let buttonView: ComponentView + if let current = self.centerButtonViews[rawValue] { + buttonView = current + } else { + buttonView = ComponentView() + self.centerButtonViews[rawValue] = buttonView + } + + let isDisabled = component.disabledTabs.contains(tab) + let isHighlighted = component.highlightedTabs.contains(tab) + let selectedVisual = component.activeTab == tab && !dontHighlightOnSelectionTabs.contains(rawValue) + let iconColor: UIColor + if selectedVisual { + iconColor = selectedIconColor + } else if isHighlighted && !isDisabled { + iconColor = accentColor + } else { + iconColor = .white + } + + let content: AnyComponent + switch tab { + case .qualityTab: + content = AnyComponent( + Image( + image: self.imageCache.qualityIcon( + isPhoto: component.qualityIsPhoto, + highQuality: component.qualityHighQuality, + preset: component.qualityPreset, + color: iconColor + ), + size: CGSize(width: 28.0, height: 22.0), + contentMode: .center + ) + ) + case .timerTab: + content = AnyComponent( + Image( + image: self.imageCache.timerIcon(value: component.timerValue, color: iconColor), + size: CGSize(width: 24.0, height: 24.0), + contentMode: .center + ) + ) + default: + content = AnyComponent( + BundleIconComponent( + name: self.iconName(tab), + tintColor: iconColor, + maxSize: CGSize(width: 28.0, height: 28.0) + ) + ) + } + let buttonSize = buttonView.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: content, + minSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide), + action: { [weak self] in + self?.tabPressed?(tab) + }, + isEnabled: component.editButtonsEnabled && !isDisabled, + animateAlpha: true, + animateScale: false, + animateContents: false + ) + ), + environment: {}, + containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide) + ) + + if let view = buttonView.view { + if view.superview == nil { + self.buttonsBackgroundView.contentView.addSubview(view) + } + let localFrame = CGRect( + x: buttonFrame.minX - backgroundFrame.minX, + y: buttonFrame.minY - backgroundFrame.minY, + width: buttonSize.width, + height: buttonSize.height + ) + if selectedVisual { + let selectionSide = max(0.0, min(backgroundFrame.width, backgroundFrame.height) - 6.0) + selectionFrame = CGRect( + x: floorToScreenPixels(localFrame.midX - selectionSide / 2.0), + y: floorToScreenPixels(localFrame.midY - selectionSide / 2.0), + width: selectionSide, + height: selectionSide + ) + } + transition.setFrame(view: view, frame: localFrame) + transition.setAlpha(view: view, alpha: isDisabled ? 0.2 : 1.0) + } + } + + if let selectionFrame = selectionFrame { + self.selectionView.layer.cornerRadius = selectionFrame.width * 0.5 + transition.setFrame(view: self.selectionView, frame: selectionFrame) + transition.setAlpha(view: self.selectionView, alpha: centerHidden ? 0.0 : 1.0) + } else { + transition.setAlpha(view: self.selectionView, alpha: 0.0) + } + } + + private func iconName(_ tab: TGPhotoEditorTab) -> String { + switch tab { + case .cropTab: + return "Media Editor/Crop" + case .toolsTab: + return "Media Editor/Adjustments" + case .rotateTab: + return "Editor/Rotate" + case .paintTab: + return "Media Editor/Pencil" + case .stickerTab: + return "Media Editor/AddSticker" + case .textTab: + return "Media Editor/AddText" + case .eraserTab: + return "Editor/Eraser" + case .mirrorTab: + return "Media Editor/Mirror" + case .aspectRatioTab: + return "Editor/AspectRatio" + case .tintTab: + return "Media Editor/Tint" + case .blurTab: + return "Media Editor/Blur" + case .curvesTab: + return "Media Editor/Curves" + default: + return "Media Editor/Crop" + } + } + + private func centerButtonFrames(tabs: [TGPhotoEditorTab], availableSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> [UInt: CGRect] { + var result: [UInt: CGRect] = [:] + let count = tabs.count + guard count != 0 else { + return result + } + + let totalLength = CGFloat(count) * toolbarButtonSide + CGFloat(count - 1) * centerButtonSpacing + let step = toolbarButtonSide + centerButtonSpacing + + if availableSize.width > availableSize.height { + let leftEdge = toolbarSideButtonSide + let rightEdge: CGFloat = component.hasSendStarsButton ? doneSize.width + 2.0 : toolbarSideButtonSide + let availableWidth = availableSize.width - leftEdge - rightEdge + let startX = floorToScreenPixels(leftEdge + (availableWidth - totalLength) / 2.0) + + for i in 0 ..< count { + result[tabs[i].rawValue] = CGRect( + x: startX + CGFloat(i) * step, + y: 0.0, + width: toolbarButtonSide, + height: toolbarButtonSide + ) + } + } else { + let x: CGFloat + if component.interfaceOrientation == .landscapeLeft { + x = availableSize.width - toolbarButtonSide - 8.0 + } else { + x = 8.0 + } + + let topInset = toolbarSideButtonSide + let bottomInset = toolbarSideButtonSide + let availableHeight = availableSize.height - topInset - bottomInset + let startY = floorToScreenPixels(topInset + (availableHeight - totalLength) / 2.0) + + for i in 0 ..< count { + result[tabs[i].rawValue] = CGRect( + x: x, + y: startY + CGFloat(i) * step, + width: toolbarButtonSide, + height: toolbarButtonSide + ) + } + } + + return result + } + + private func updateInfoLabel(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, transition: ComponentTransition) { + self.infoLabel.text = component.infoString + self.infoLabel.isHidden = component.infoString == nil + guard component.infoString != nil else { + return + } + + if availableSize.width > availableSize.height { + self.infoLabel.transform = .identity + transition.setFrame(view: self.infoLabel, frame: CGRect(x: toolbarSideButtonSide + 10.0, y: 0.0, width: availableSize.width - (toolbarSideButtonSide + 10.0) * 2.0, height: toolbarSideButtonSide)) + } else { + let bounds = CGRect(x: 0.0, y: 0.0, width: availableSize.height - (toolbarSideButtonSide + 10.0) * 2.0, height: availableSize.width) + self.infoLabel.bounds = bounds + self.infoLabel.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + if component.interfaceOrientation == .landscapeLeft { + self.infoLabel.transform = CGAffineTransform(rotationAngle: .pi / 2.0) + } else if component.interfaceOrientation == .landscapeRight { + self.infoLabel.transform = CGAffineTransform(rotationAngle: -.pi / 2.0) + } else { + self.infoLabel.transform = .identity + } + } + } + } + + func makeView() -> View { + return View(frame: .zero) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public func makeMediaPickerPhotoToolbarView(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) -> (UIView & TGPhotoToolbarViewProtocol)? { + return MediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) +} + +final class MediaPickerPhotoToolbarView: UIView, TGPhotoToolbarViewProtocol { + private let context: AccountContext + private let solidBackground: Bool + private let hasSendStarsButton: Bool + private let rootView = ComponentView() + + private var transitionedOut = false + + var cancelPressed: (() -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var donePressed: (() -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var doneLongPressed: ((Any?) -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var tabPressed: ((TGPhotoEditorTab) -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + + var interfaceOrientation: UIInterfaceOrientation = .portrait { + didSet { + self.update(transition: .immediate) + } + } + + var bottomInset: CGFloat = 0.0 + + var backButtonType: TGPhotoEditorBackButton { + didSet { + self.update(transition: .immediate) + } + } + + var doneButtonType: TGPhotoEditorDoneButton { + didSet { + self.update(transition: .immediate) + } + } + + var sendPaidMessageStars: Int64 = 0 { + didSet { + self.update(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + } + + private(set) var currentTabs: TGPhotoEditorTab = [] + private var activeTab: TGPhotoEditorTab = [] + private var highlightedTabs: TGPhotoEditorTab = [] + private var disabledTabs: TGPhotoEditorTab = [] + private var editButtonsHidden = false + private var editButtonsEnabled = true + private var centerButtonsHidden = false + private var allButtonsHidden = false + private var cancelDoneButtonsHidden = false + private var doneButtonEnabled = true + private var qualityIsPhoto = false + private var qualityHighQuality = false + private var qualityPreset = 3 + private var timerValue = 0 + private var infoString: String? + + var doneButton: UIView { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonSourceView ?? UIView() + } + + var cancelButtonFrame: CGRect { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.cancelButtonFrame ?? .zero + } + + var doneButtonFrame: CGRect { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonFrame ?? .zero + } + + init(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) { + self.context = context + self.backButtonType = backButton + self.doneButtonType = doneButton + self.solidBackground = solidBackground + self.hasSendStarsButton = hasSendStarsButton + + super.init(frame: .zero) + + self.clipsToBounds = false + self.backgroundColor = .clear + self.update(transition: .immediate) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.update(transition: .immediate) + self.updateRootFrame(transition: .immediate) + } + + func transitionIn(animated: Bool) { + self.transitionIn(animated: animated, transparent: false) + } + + func transitionIn(animated: Bool, transparent: Bool) { + self.transitionedOut = false + self.isHidden = false + if !transparent { + self.backgroundColor = UIColor(rgb: 0x000000) + } + + let transition = animated ? ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate + self.updateRootFrame(transition: transition) + + if !transparent { + self.backgroundColor = .clear + } + } + + func transitionOut(animated: Bool) { + self.transitionOut(animated: animated, transparent: false, hideOnCompletion: false) + } + + func transitionOut(animated: Bool, transparent: Bool, hideOnCompletion: Bool) { + self.transitionedOut = true + if !transparent { + self.backgroundColor = UIColor(rgb: 0x000000) + } + + let duration: Double = animated ? 0.3 : 0.0 + let transition = animated ? ComponentTransition(animation: .curve(duration: duration, curve: .easeInOut)) : .immediate + self.updateRootFrame(transition: transition) + + if hideOnCompletion { + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in + self?.isHidden = true + } + } + } + + func setDoneButtonEnabled(_ enabled: Bool, animated: Bool) { + self.doneButtonEnabled = enabled + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsEnabled(_ enabled: Bool, animated: Bool) { + self.editButtonsEnabled = enabled + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsHidden(_ hidden: Bool, animated: Bool) { + self.editButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsHighlighted(_ buttons: TGPhotoEditorTab) { + self.highlightedTabs = buttons + self.update(transition: .immediate) + } + + func setEditButtonsDisabled(_ buttons: TGPhotoEditorTab) { + self.disabledTabs = buttons + self.update(transition: .immediate) + } + + func setCenterButtonsHidden(_ hidden: Bool, animated: Bool) { + self.centerButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setAllButtonsHidden(_ hidden: Bool, animated: Bool) { + self.allButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setCancelDoneButtonsHidden(_ hidden: Bool, animated: Bool) { + self.cancelDoneButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setToolbarTabs(_ tabs: TGPhotoEditorTab, animated: Bool) { + self.currentTabs = tabs + self.update(transition: self.transition(animated: animated)) + } + + func setActiveTab(_ tab: TGPhotoEditorTab) { + self.activeTab = tab + self.update(transition: .spring(duration: 0.4)) + } + + func setQualityButtonIsPhoto(_ isPhoto: Bool, highQuality: Bool, videoPreset: Int) { + self.qualityIsPhoto = isPhoto + self.qualityHighQuality = highQuality + self.qualityPreset = videoPreset + self.update(transition: .immediate) + } + + func setTimerButtonValue(_ value: Int) { + self.timerValue = value + self.update(transition: .immediate) + } + + func setInfoString(_ string: String?) { + self.infoString = string + self.update(transition: .immediate) + } + + @objc(viewForTab:) + func view(for tab: TGPhotoEditorTab) -> UIView? { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.viewForTab(tab) + } + + private func transition(animated: Bool) -> ComponentTransition { + if animated { + return ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } else { + return .immediate + } + } + + private func update(transition: ComponentTransition) { + let size = self.bounds.size + let _ = self.rootView.update( + transition: transition, + component: AnyComponent( + MediaPickerPhotoToolbarComponent( + context: self.context, + solidBackground: self.solidBackground, + backButtonType: self.backButtonType, + doneButtonType: self.doneButtonType, + currentTabs: self.currentTabs, + activeTab: self.activeTab, + highlightedTabs: self.highlightedTabs, + disabledTabs: self.disabledTabs, + qualityIsPhoto: self.qualityIsPhoto, + qualityHighQuality: self.qualityHighQuality, + qualityPreset: self.qualityPreset, + timerValue: self.timerValue, + hasSendStarsButton: self.hasSendStarsButton, + sendPaidMessageStars: self.sendPaidMessageStars, + editButtonsHidden: self.editButtonsHidden, + editButtonsEnabled: self.editButtonsEnabled, + centerButtonsHidden: self.centerButtonsHidden, + allButtonsHidden: self.allButtonsHidden, + cancelDoneButtonsHidden: self.cancelDoneButtonsHidden, + doneButtonEnabled: self.doneButtonEnabled, + interfaceOrientation: self.interfaceOrientation, + bottomInset: self.bottomInset, + infoString: self.infoString, + cancelPressed: self.cancelPressed, + donePressed: self.donePressed, + doneLongPressed: self.doneLongPressed, + tabPressed: self.tabPressed + ) + ), + environment: {}, + forceUpdate: true, + containerSize: size + ) + + if let view = self.rootView.view { + if view.superview == nil { + self.addSubview(view) + } + self.updateRootFrame(transition: transition) + } + } + + private func updateRootFrame(transition: ComponentTransition) { + guard let view = self.rootView.view else { + return + } + + var frame = CGRect(origin: .zero, size: self.bounds.size) + if self.transitionedOut { + if self.bounds.width > self.bounds.height { + frame.origin.y = self.bounds.height + } else if self.interfaceOrientation == .landscapeLeft { + frame.origin.x = -self.bounds.width + } else { + frame.origin.x = self.bounds.width + } + } + transition.setFrame(view: view, frame: frame) + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 142ad3d7dc..a95317f79d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -15,7 +15,6 @@ import LegacyComponents import LegacyMediaPickerUI import AttachmentUI import ContextUI -import WebSearchUI import SparseItemGrid import UndoUI import PresentationDataUtils @@ -192,7 +191,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att case addImage case cover case createSticker - case createAvatar + case createAvatar(mode: PeerType) case poll(mode: PollMode, asFile: Bool) } @@ -244,12 +243,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let moreButtonNode: MoreButtonNode private let selectedButtonNode: SelectedButtonNode - public weak var webSearchController: WebSearchController? - public var openCamera: ((Any?) -> Void)? public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } - public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } public var openBoost: () -> Void = { } @@ -705,8 +701,13 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att var useModernCamera = false if case .assets(nil, .default) = controller.subject { useLegacyCamera = true - } else if case .assets(nil, let mode) = controller.subject, [.createSticker, .createAvatar].contains(mode) { - useModernCamera = true + } else if case .assets(nil, let mode) = controller.subject { + switch mode { + case .createSticker, .createAvatar: + useModernCamera = true + default: + break + } } if useLegacyCamera { @@ -1517,28 +1518,32 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } - private func openLimitedMediaOptions() { - let presentationData = self.presentationData - let controller = ActionSheetController(presentationData: self.presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() + private func openLimitedMediaOptions(sourceView: UIView) { + guard let controller = self.controller else { + return } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessSelectMore, color: .accent, action: { [weak self] in - dismissAction() - if #available(iOS 14.0, *), let strongController = self?.controller { - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: strongController) - } - }), - ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessChangeSettings, color: .accent, action: { [weak self] in - dismissAction() - self?.controller?.context.sharedContext.applicationBindings.openSettings() - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - self.controller?.present(controller, in: .window(.root)) + + let presentationData = self.presentationData + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.Media_LimitedAccessSelectMore, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Image"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + if #available(iOS 14.0, *), let controller = self?.controller { + PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller) + } + })), + .action(ContextMenuActionItem(text: presentationData.strings.Media_LimitedAccessChangeSettings, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.controller?.context.sharedContext.applicationBindings.openSettings() + })) + ] + let contextController = makeContextController(presentationData: presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) } private func getItemSnapshot(_ identifier: String) -> UIView? { @@ -1770,9 +1775,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att manageNode = current } else { manageNode = MediaPickerManageNode() - manageNode.pressed = { [weak self] in + manageNode.pressed = { [weak self] sourceView in if let strongSelf = self { - strongSelf.openLimitedMediaOptions() + strongSelf.openLimitedMediaOptions(sourceView: sourceView) } } self.manageNode = manageNode @@ -1788,7 +1793,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att manageNode = current } else { manageNode = MediaPickerManageNode() - manageNode.pressed = { [weak self] in + manageNode.pressed = { [weak self] _ in self?.controller?.context.sharedContext.applicationBindings.openSettings() } self.manageNode = manageNode @@ -2070,9 +2075,16 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.subtitle = presentationData.strings.MediaPicker_CreateSticker self.titleView.isEnabled = true - case .createAvatar: + case let .createAvatar(avatarMode): self.titleView.title = presentationData.strings.MediaPicker_Recents - self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewPhoto + switch avatarMode { + case .user: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewPhoto + case .group: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewGroupPhoto + case .channel: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewChannelPhoto + } self.titleView.isEnabled = true case .story: self.titleView.title = presentationData.strings.MediaPicker_Recents @@ -2209,21 +2221,13 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.scrollToTop = { [weak self] in if let strongSelf = self { - if let webSearchController = strongSelf.webSearchController { - webSearchController.scrollToTop?() - } else { - strongSelf.controllerNode.scrollToTop(animated: true) - } + strongSelf.controllerNode.scrollToTop(animated: true) } } self.scrollToTopWithTabBar = { [weak self] in if let strongSelf = self { - if let webSearchController = strongSelf.webSearchController { - webSearchController.cancel() - } else { - strongSelf.scrollToTop?() - } + strongSelf.scrollToTop?() } } @@ -2822,7 +2826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.updateNavigationStack { current in var mediaPickerContext: AttachmentMediaPickerContext? if let first = current.first as? MediaPickerScreenImpl { - mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext + mediaPickerContext = first.mediaPickerContext } return (current.filter { $0 !== self }, mediaPickerContext) } @@ -2905,10 +2909,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } public func resetForReuse() { - if let webSearchController = self.webSearchController { - self.webSearchController = nil - webSearchController.dismiss() - } self.scrollToTop?() self.controllerNode.isSuspended = true @@ -2971,8 +2971,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if case .story = mode { self.updateNavigationStack({ _ in return ([self, groupsController], self.mediaPickerContext)}) - } else { - self.presentWebSearch(groupsController, activateOnDisplay) } self.groupsController = groupsController } @@ -3589,26 +3587,6 @@ public func mediaPickerController( completion(result) controller.dismiss(animated: true) } - if hasSearch { - mediaPickerController.presentWebSearch = { [weak mediaPickerController] groups, activateOnDisplay in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).start(next: { configuration in - let webSearchController = WebSearchController( - context: context, - updatedPresentationData: updatedPresentationData, - peer: nil, - chatLocation: nil, - configuration: configuration, - mode: .editor(completion: { [weak mediaPickerController] image in - completion(image) - mediaPickerController?.dismiss(animated: true) - }), - activateOnDisplay: activateOnDisplay - ) - mediaPickerController?.present(webSearchController, in: .current) - }) - } - } present(mediaPickerController, mediaPickerController.mediaPickerContext) return true } @@ -3909,6 +3887,7 @@ public func stickerMediaPickerController( public func avatarMediaPickerController( context: AccountContext, + peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, @@ -4022,7 +4001,7 @@ public func avatarMediaPickerController( chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, - subject: .assets(nil, .createAvatar), + subject: .assets(nil, .createAvatar(mode: peerType)), mainButtonState: mainButtonState, mainButtonAction: { [weak controller] in controller?.dismiss(animated: true) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 31622e2b02..2f58275139 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -663,6 +663,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.panGestureRecognizer.cancelsTouchesInView = true self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false self.view.addGestureRecognizer(ReorderingGestureRecognizer(animateOnTouch: !self.persistentItems, shouldBegin: { [weak self] point in if let strongSelf = self, !strongSelf.scrollNode.view.isDragging && strongSelf.itemNodes.count > 1 { diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift b/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift index 9af464ac9d..1fc779008e 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift @@ -3,6 +3,7 @@ import UIKit import TelegramCore import SwiftSignalKit import Display +import TelegramPresentationData public struct OpenInAppIconResourceId { public let appStoreId: Int64 @@ -131,7 +132,7 @@ private func drawOpenInAppIconBorder(into c: CGContext, arguments: TransformImag c.strokePath() } -public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon, withChrome: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { switch appIcon { case let .resource(resource): return openInAppIconData(engine: engine, appIcon: resource) |> map { data in @@ -166,14 +167,17 @@ public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon) -> Sig guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } - + context.withFlippedContext { c in c.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: arguments.drawingSize)) - drawOpenInAppIconBorder(into: c, arguments: arguments) + if withChrome { + drawOpenInAppIconBorder(into: c, arguments: arguments) + } } - addCorners(context, arguments: arguments) - + if withChrome { + addCorners(context, arguments: arguments) + } return context }) } diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 7721f5bdb1..dc427f26f2 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -90,24 +90,20 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope var options: [OpenInOption] = [] switch item { case let .url(url): - var skipSafari = false if url.contains("youtube.com/") || url.contains("youtu.be/") { let updatedUrl = url.replacingOccurrences(of: "https://", with: "youtube://").replacingOccurrences(of: "http://", with: "youtube://") options.append(OpenInOption(identifier: "youtube", application: .other(title: "YouTube", identifier: 544007664, scheme: "youtube", store: nil), action: { return .openUrl(url: updatedUrl) })) - skipSafari = true } - if !skipSafari { - options.append(OpenInOption(identifier: "safari", application: .safari, action: { - var url = url - if url.hasPrefix("https://") { - url = url.replacingOccurrences(of: "https://", with: "x-safari-https://") - } - return .openUrl(url: url) - })) - } + options.append(OpenInOption(identifier: "safari", application: .safari, action: { + var url = url + if url.hasPrefix("https://") { + url = url.replacingOccurrences(of: "https://", with: "x-safari-https://") + } + return .openUrl(url: url) + })) options.append(OpenInOption(identifier: "chrome", application: .other(title: "Chrome", identifier: 535886823, scheme: "googlechrome", store: nil), action: { if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) { @@ -202,6 +198,10 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: { return .openUrl(url: "alook://\(url)") })) + + options.append(OpenInOption(identifier: "vivaldi", application: .other(title: "Vivaldi", identifier: 1633234600, scheme: "vivaldi", store: "us"), action: { + return .openUrl(url: "vivaldi://\(url)") + })) case let .location(location, directions): let lat = location.latitude let lon = location.longitude @@ -334,6 +334,14 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope return .openUrl(url: url) } })) + + options.append(OpenInOption(identifier: "yandexGo", application: .other(title: "Yandex Go", identifier: 472650686, scheme: "yandextaxi", store: nil), action: { + return .openUrl(url: "yandextaxi://route?end-lat=\(lat)&end-lon=\(lon)") + })) + + options.append(OpenInOption(identifier: "yango", application: .other(title: "Yango", identifier: 1437157286, scheme: "yangoride", store: nil), action: { + return .openUrl(url: "yangoride://route?end-lat=\(lat)&end-lon=\(lon)") + })) } return options } diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift similarity index 92% rename from submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift rename to submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift index 7ab3ada31f..d1825cb09f 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift @@ -25,7 +25,7 @@ public struct OpenInControllerAction { } } -public final class OpenInActionSheetController: ViewControllerComponentContainer { +public final class OpenInOptionsScreen: ViewControllerComponentContainer { public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -69,7 +69,7 @@ public final class OpenInActionSheetController: ViewControllerComponentContainer super.init( context: context, - component: OpenInActionSheetScreenComponent( + component: OpenInOptionsScreenComponent( context: context, options: availableOpenInOptions(context: context, item: item), additionalAction: additionalAction, @@ -95,7 +95,7 @@ public final class OpenInActionSheetController: ViewControllerComponentContainer } } -private final class OpenInActionSheetScreenComponent: Component { +private final class OpenInOptionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext @@ -115,7 +115,7 @@ private final class OpenInActionSheetScreenComponent: Component { self.invokeAction = invokeAction } - static func ==(lhs: OpenInActionSheetScreenComponent, rhs: OpenInActionSheetScreenComponent) -> Bool { + static func ==(lhs: OpenInOptionsScreenComponent, rhs: OpenInOptionsScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -156,7 +156,7 @@ private final class OpenInActionSheetScreenComponent: Component { } } - func update(component: OpenInActionSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: OpenInOptionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -175,7 +175,7 @@ private final class OpenInActionSheetScreenComponent: Component { let _ = self.sheet.update( transition: transition, component: AnyComponent(SheetComponent( - content: AnyComponent(OpenInActionSheetContentComponent( + content: AnyComponent(OpenInOptionsSheetContentComponent( context: component.context, options: component.options, additionalAction: component.additionalAction, @@ -217,7 +217,7 @@ private final class OpenInActionSheetScreenComponent: Component { } } -private final class OpenInActionSheetContentComponent: Component { +private final class OpenInOptionsSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext @@ -240,7 +240,7 @@ private final class OpenInActionSheetContentComponent: Component { self.dismiss = dismiss } - static func ==(lhs: OpenInActionSheetContentComponent, rhs: OpenInActionSheetContentComponent) -> Bool { + static func ==(lhs: OpenInOptionsSheetContentComponent, rhs: OpenInOptionsSheetContentComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -281,7 +281,7 @@ private final class OpenInActionSheetContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: OpenInActionSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: OpenInOptionsSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let theme = environment.theme let strings = environment.strings @@ -318,7 +318,7 @@ private final class OpenInActionSheetContentComponent: Component { component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Map_OpenIn, - font: Font.medium(20.0), + font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center )), @@ -343,7 +343,7 @@ private final class OpenInActionSheetContentComponent: Component { let optionInset: CGFloat = 2.0 let optionSize = CGSize(width: 80.0, height: 112.0) - let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: 64.0), size: CGSize(width: availableSize.width, height: optionSize.height)) + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: 82.0), size: CGSize(width: availableSize.width, height: optionSize.height)) transition.setFrame(view: self.scrollView, frame: scrollFrame) var validIds = Set() @@ -365,8 +365,8 @@ private final class OpenInActionSheetContentComponent: Component { }) let optionOriginX: CGFloat - if component.options.count == 1 { - optionOriginX = floorToScreenPixels(max(0.0, (availableSize.width - optionSize.width) / 2.0)) + if component.options.count < 5 { + optionOriginX = floorToScreenPixels(max(0.0, (availableSize.width - optionSize.width * CGFloat(component.options.count)) / 2.0)) } else { optionOriginX = optionInset + CGFloat(index) * optionSize.width } @@ -392,7 +392,7 @@ private final class OpenInActionSheetContentComponent: Component { } self.scrollView.contentSize = CGSize(width: max(availableSize.width, optionsContentWidth), height: optionSize.height) - var contentHeight = scrollFrame.maxY + 18.0 + var contentHeight = scrollFrame.maxY + 22.0 if let additionalAction = component.additionalAction { let shareButton: ComponentView if let current = self.shareButton { @@ -474,7 +474,7 @@ private final class OpenInAppView: UIControl { private var action: (() -> Void)? override init(frame: CGRect) { - self.iconView = TransformImageView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0))) + self.iconView = TransformImageView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 64.0, height: 64.0))) self.titleLabel = UILabel() super.init(frame: frame) @@ -499,9 +499,9 @@ private final class OpenInAppView: UIControl { func update(context: AccountContext, theme: PresentationTheme, option: OpenInOption, action: @escaping () -> Void) { self.action = action self.accessibilityLabel = option.title - self.titleLabel.attributedText = NSAttributedString(string: option.title, font: Font.regular(11.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + self.titleLabel.attributedText = NSAttributedString(string: option.title, font: Font.medium(12.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let iconSize = CGSize(width: 60.0, height: 60.0) + let iconSize = CGSize(width: 64.0, height: 64.0) let makeLayout = self.iconView.asyncLayout() let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets())) applyLayout() @@ -531,7 +531,7 @@ private final class OpenInAppView: UIControl { override func layoutSubviews() { super.layoutSubviews() - self.iconView.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 60.0, height: 60.0)) - self.titleLabel.frame = CGRect(origin: CGPoint(x: 0.0, y: 78.0), size: CGSize(width: self.bounds.width, height: 16.0)) + self.iconView.frame = CGRect(origin: CGPoint(x: 8.0, y: 12.0), size: CGSize(width: 64.0, height: 64.0)) + self.titleLabel.frame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: CGSize(width: self.bounds.width, height: 16.0)) } } diff --git a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift index cc9c224885..ee691a9d42 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift @@ -51,6 +51,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { self.addSubnode(self.activityIndicator) self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) self.backgroundColor = presentationData.theme.list.blocksBackgroundColor diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index cf77e35974..aea80e2cf4 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -1819,6 +1819,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, ASS self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index cbbe116a36..0ee49ffed5 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -34,7 +34,6 @@ swift_library( "//submodules/LegacyUI:LegacyUI", "//submodules/GalleryUI:GalleryUI", "//submodules/ShareController:ShareController", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/SearchBarNode:SearchBarNode", "//submodules/ContactsPeerItem:ContactsPeerItem", "//submodules/PasswordSetupUI:PasswordSetupUI", diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 57dd562daf..96110577b5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -14,6 +14,7 @@ import PresentationDataUtils import ItemListAvatarAndNameInfoItem import OldChannelsController import ChatTimerScreen +import ContextUI private let rankMaxLength: Int32 = 16 @@ -55,6 +56,7 @@ private enum ChannelBannedMemberSection: Int32 { private enum ChannelBannedMemberEntryTag: ItemListItemTag { case rank + case timeout func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ChannelBannedMemberEntryTag, self == other { @@ -362,7 +364,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { case let .timeout(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openTimeout() - }) + }, tag: ChannelBannedMemberEntryTag.timeout) case let .exceptionInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .delete(_, text): @@ -408,6 +410,18 @@ private struct ChannelBannedMemberControllerState: Equatable { var focusedOnRank: Bool } +private final class ChannelBannedMemberContextReferenceContentSource: 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)) + } +} + func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { var result = flags result.remove(.banReadMessages) @@ -604,10 +618,12 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissInputImpl: (() -> Void)? var errorImpl: (() -> Void)? var scrollToRankImpl: (() -> Void)? + var findTimeoutReferenceNode: (() -> ItemListDisclosureItemNode?)? let peerSignal = Promise() peerSignal.set(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) @@ -715,12 +731,20 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } }, openTimeout: { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) let intervals: [Int32] = [ 1 * 60 * 60 * 24, 7 * 60 * 60 * 24, 30 * 60 * 60 * 24 ] + let currentTimeout: Int32 = stateValue.with { state in + if let updatedTimeout = state.updatedTimeout { + return updatedTimeout + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { + return banInfo.rights.untilDate + } else { + return Int32.max + } + } let applyValue: (Int32?) -> Void = { value in updateState { state in var state = state @@ -728,19 +752,34 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen return state } } - var items: [ActionSheetItem] = [] + var items: [ContextMenuItem] = [] for interval in intervals { - items.append(ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: interval), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - applyValue(initialState.referenceTimestamp + interval) - })) + let timeoutValue = initialState.referenceTimestamp + interval + items.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: presentationData.strings, value: interval), icon: { theme in + if currentTimeout == timeoutValue { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) + applyValue(timeoutValue) + }))) } - items.append(ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Forever, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Forever, icon: { theme in + if currentTimeout == 0 || currentTimeout == Int32.max { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) applyValue(Int32.max) - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Custom, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Custom, icon: { _ in + return nil + }, action: { _, f in + f(.default) let controller = ChatTimerScreen( context: context, updatedPresentationData: updatedPresentationData, @@ -763,13 +802,21 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } ) presentControllerImpl?(controller, nil) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) + }))) + guard let sourceNode = findTimeoutReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(ChannelBannedMemberContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, delete: { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) @@ -1104,9 +1151,15 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + presentInGlobalOverlayImpl = { [weak controller] value in + controller?.presentInGlobalOverlay(value, with: nil) + } pushControllerImpl = { [weak controller] c in controller?.push(c) } + findTimeoutReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: ChannelBannedMemberEntryTag.timeout) as? ItemListDisclosureItemNode + } let hapticFeedback = HapticFeedback() errorImpl = { [weak controller] in diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift index 64133463f7..3683dcf37b 100644 --- a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -383,7 +383,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { imageNode.update(size: imageFitSize) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size) if let switchView = strongSelf.switchNode.view as? UISwitch { if strongSelf.switchNode.bounds.size.width.isZero { switchView.sizeToFit() diff --git a/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift b/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift index 985a4bf850..7f75d032ee 100644 --- a/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift @@ -73,6 +73,8 @@ final class SecretChatKeyControllerNode: ViewControllerTracingNode { override func didLoad() { super.didLoad() + self.scrollNode.view.scrollsToTop = false + self.infoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.infoTap(_:)))) } diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 0217b4dd9c..e9fe7c4fd2 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -12,11 +12,12 @@ import ViewControllerComponent import SheetComponent import MultilineTextComponent import BundleIconComponent -import SolidRoundedButtonComponent +import ButtonComponent import BlurredBackgroundComponent import Markdown import TelegramUIPreferences import GlassBarButtonComponent +import LottieComponent public final class PremiumGradientBackgroundComponent: Component { public let colors: [UIColor] @@ -671,7 +672,7 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = Child(GlassBarButtonComponent.self) let background = Child(PremiumGradientBackgroundComponent.self) let pager = Child(DemoPagerComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) let measureText = Child(MultilineTextComponent.self) return { context in @@ -1326,28 +1327,46 @@ private final class DemoSheetContent: CombinedComponent { } let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let premiumGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ButtonTextContentComponent( + text: buttonText, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumGradientColors[0] + )))) + if isStandalone, let buttonAnimationName { + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: buttonAnimationName), + color: .white, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumGradientColors[0], + foreground: .white, + pressedColor: premiumGradientColors[0], + isShimmering: state.isPremium != true, + gradient: ButtonComponent.Background.Gradient( + colors: premiumGradientColors, + animation: .horizontalShift(duration: 4.5) + ) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonText)-\(isStandalone ? buttonAnimationName ?? "" : "")"), + component: AnyComponent(HStack(buttonTitle, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: state.isPremium != true, - glass: true, - animationName: isStandalone ? buttonAnimationName : nil, - iconPosition: .right, - iconSpacing: 4.0, action: { [weak component, weak state] in guard let component = component else { return diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index c2d9c19c5b..7d95e05186 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -8,10 +8,9 @@ import TelegramPresentationData import PresentationDataUtils import ViewControllerComponent import AccountContext -import SolidRoundedButtonComponent import MultilineTextComponent import BundleIconComponent -import SolidRoundedButtonComponent +import ButtonComponent import BlurredBackgroundComponent import Markdown import InAppPurchaseManager @@ -1015,7 +1014,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let secondaryTitle = Child(MultilineTextComponent.self) let bottomPanel = Child(BlurredBackgroundComponent.self) let bottomSeparator = Child(Rectangle.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self].value @@ -1233,28 +1232,38 @@ private final class PremiumGiftScreenComponent: CombinedComponent { buttonText = environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string } + let buttonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: UIColor(rgb: 0x8878ff), - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonGradientColors[0], + foreground: .white, + pressedColor: buttonGradientColors[0], + isShimmering: gloss, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) ), - height: 50.0, - cornerRadius: 11.0, - gloss: gloss, - isLoading: state.inProgress, + content: AnyComponentWithIdentity( + id: AnyHashable(buttonText), + component: AnyComponent(ButtonTextContentComponent( + text: buttonText, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )) + ), + displaysProgress: state.inProgress, action: { state.buy() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 52.0), transition: context.transition) let bottomPanel = bottomPanel.update( diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index afd1743d89..e825015ca7 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -10,7 +10,6 @@ import TelegramPresentationData import PresentationDataUtils import ViewControllerComponent import AccountContext -import SolidRoundedButtonComponent import ButtonComponent import MultilineTextComponent import MultilineTextWithEntitiesComponent @@ -3416,7 +3415,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let title = Child(MultilineTextComponent.self) let secondaryTitle = Child(MultilineTextWithEntitiesComponent.self) let bottomEdgeEffect = Child(EdgeEffectComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) var updatedInstalled: Bool? @@ -3825,25 +3824,51 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let controller = environment.controller + let buttonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + let buttonContent: AnyComponent + if let buttonSubtitle { + buttonContent = AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(Text( + text: buttonTitle, + font: Font.semibold(17.0), + color: .white + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(Text( + text: buttonSubtitle, + font: Font.medium(11.0), + color: UIColor.white.withAlphaComponent(0.7) + ))) + ], spacing: 1.0)) + } else { + buttonContent = AnyComponent(ButtonTextContentComponent( + text: buttonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonTitle, - subtitle: buttonSubtitle, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: UIColor(rgb: 0x8878ff), - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: UIColor(rgb: 0x8878ff), + foreground: .white, + pressedColor: UIColor(rgb: 0x8878ff).withMultipliedAlpha(0.8), + cornerRadius: 26.0, + isShimmering: true, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) ), - height: 52.0, - cornerRadius: 26.0, - gloss: true, - glass: true, - isLoading: state.inProgress, + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonTitle)-\(buttonSubtitle ?? "")"), + component: buttonContent + ), + displaysProgress: state.inProgress, action: { if let controller = controller() as? PremiumIntroScreen, let customProceed = controller.customProceed { controller.dismiss() diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index b487f1ea42..d60ecf31bd 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -13,6 +13,8 @@ import SheetComponent import MultilineTextComponent import BundleIconComponent import SolidRoundedButtonComponent +import ButtonComponent +import LottieComponent import Markdown import BalancedTextComponent import ConfettiEffect @@ -832,7 +834,7 @@ private final class LimitSheetContent: CombinedComponent { let alternateText = Child(List.self) let limit = Child(PremiumLimitDisplayComponent.self) let linkButton = Child(SolidRoundedButtonComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) let peerShortcut = Child(Button.self) let statsButton = Child(Button.self) @@ -1456,23 +1458,44 @@ private final class LimitSheetContent: CombinedComponent { let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonTitle = actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK) + var buttonContentItems: [AnyComponentWithIdentity] = [] + if let buttonIconName { + buttonContentItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent( + name: buttonIconName, + tintColor: .white + )))) + } + buttonContentItems.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(ButtonTextContentComponent( + text: buttonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )))) + if buttonIconName == nil, isIncreaseButton, let buttonAnimationName { + buttonContentItems.append(AnyComponentWithIdentity(id: "animation", component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: buttonAnimationName), + color: .white, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK), - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: buttonGradientColors, - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonGradientColors[0], + foreground: .white, + pressedColor: buttonGradientColors[0], + isShimmering: isIncreaseButton && actionButtonHasGloss, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonTitle)-\(buttonIconName ?? "")-\(isIncreaseButton ? buttonAnimationName ?? "" : "")"), + component: AnyComponent(HStack(buttonContentItems, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: isIncreaseButton && actionButtonHasGloss, - glass: true, - iconName: buttonIconName, - animationName: isIncreaseButton ? buttonAnimationName : nil, - iconPosition: buttonIconName != nil ? .left : .right, action: { if isIncreaseButton { if component.action() { diff --git a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift index 7fc6496107..af9c412f2b 100644 --- a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift @@ -13,6 +13,7 @@ import BundleIconComponent import BalancedTextComponent import MultilineTextComponent import SolidRoundedButtonComponent +import ButtonComponent import LottieComponent import AccountContext import GlassBarButtonComponent @@ -110,7 +111,7 @@ private final class SheetContent: CombinedComponent { let premiumTitle = Child(BalancedTextComponent.self) let premiumText = Child(BalancedTextComponent.self) - let premiumButton = Child(SolidRoundedButtonComponent.self) + let premiumButton = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -342,28 +343,32 @@ private final class SheetContent: CombinedComponent { contentSize.height += premiumText.size.height contentSize.height += spacing + 5.0 + let premiumGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let premiumButton = premiumButton.update( - component: SolidRoundedButtonComponent( - title: premiumButtonTitle, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumGradientColors[0], + foreground: .white, + pressedColor: premiumGradientColors[0], + isShimmering: false, + gradient: ButtonComponent.Background.Gradient(colors: premiumGradientColors) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(premiumButtonTitle), + component: AnyComponent(ButtonTextContentComponent( + text: premiumButtonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumGradientColors[0] + )) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: false, - glass: true, - iconName: nil, - animationName: nil, - iconPosition: .left, action: { component.openPremiumIntro() component.dismiss() diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 5a6b093777..bd014a9bdb 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -335,6 +335,7 @@ private class StickersCarouselNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.canCancelContentTouches = true + self.scrollNode.view.scrollsToTop = false self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerTapped(_:)))) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 89d3a1cee3..bcb2e7d26d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -533,6 +533,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.clipsToBounds = false if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 9f982bd893..b8a0540a12 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -68,7 +68,6 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/UrlEscaping:UrlEscaping", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/UrlHandling:UrlHandling", "//submodules/HexColor:HexColor", "//submodules/QrCode:QrCode", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 8b2ab482a5..e02fd7938c 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -133,6 +133,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.alwaysBounceHorizontal = false + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift index 4d75e9a206..cd00e39271 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -297,15 +297,9 @@ public func storageUsageExceptionsScreen( } } - 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() @@ -466,9 +460,6 @@ public func storageUsageExceptionsScreen( 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) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift index ecaa103547..44f4a7c967 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift @@ -11,22 +11,24 @@ import OpenInExternalAppUI import AccountContext import AppBundle -class WebBrowserItem: ListViewItem, ItemListItem { +final class WebBrowserItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle let title: String let application: OpenInApplication? + let identifier: String? let checked: Bool public let sectionId: ItemListSectionId let action: () -> Void - public init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, title: String, application: OpenInApplication?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, title: String, application: OpenInApplication?, identifier: String?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.context = context self.presentationData = presentationData self.systemStyle = systemStyle self.title = title self.application = application + self.identifier = identifier self.checked = checked self.sectionId = sectionId self.action = action @@ -148,11 +150,16 @@ private final class WebBrowserItemNode: ListViewItemNode { if currentItem == nil { switch item.application { case .none: - let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons() - let current = item.context.sharedContext.applicationBindings.getAlternateIconName() - let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon" - if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) { - updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) + if item.identifier == "default" { + let image = renderSettingsIcon(name: "Chat/Context Menu/Globe", backgroundColors: [UIColor(rgb: 0x8E8E93)])! + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image), withChrome: false) + } else { + let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons() + let current = item.context.sharedContext.applicationBindings.getAlternateIconName() + let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon" + if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) { + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) + } } case .safari: if let image = UIImage(bundleImageName: "Open In/Safari") { diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 58751f9d37..7eafa1200a 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -271,7 +271,7 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case let .browserHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .browser(_, title, application, identifier, selected, _): - return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, checked: selected, sectionId: self.section) { + return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, identifier: identifier, checked: selected, sectionId: self.section) { arguments.updateDefaultBrowser(identifier) } case let .browserInfo(_, text): @@ -326,11 +326,11 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen var entries: [WebBrowserSettingsControllerEntry] = [] let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) - let defaultExternalBrowser = localSettings.defaultWebBrowser ?? "safari" + let defaultExternalBrowser = localSettings.defaultWebBrowser ?? "default" entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title)) entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, !accountSettings.openExternalBrowser, 0)) - + var index: Int32 = 1 for option in options { entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, accountSettings.openExternalBrowser && option.identifier == defaultExternalBrowser, index)) diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index 631c325844..103e3af4b7 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -53,7 +53,7 @@ public class LocalizationListController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) - self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + self.editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 1db0fbe0a9..ed9aef7f0c 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -677,7 +677,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { var listInsets = layout.insets(options: [.input]) listInsets.top += navigationBarHeight - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) listInsets.left += inset listInsets.right += inset diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift index 2759bfb669..6c97429dc4 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift @@ -43,7 +43,7 @@ public class NotificationExceptionsController: ViewController { self._hasGlassStyle = true self.removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed)) - self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + self.editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style @@ -98,7 +98,7 @@ public class NotificationExceptionsController: ViewController { self.controllerNode.updatePresentationData(self.presentationData) let removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed)) - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + let editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) if self.navigationItem.rightBarButtonItem === self.editItem { self.navigationItem.rightBarButtonItem = editItem diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index b656ea640d..8fa2031bbd 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -14,6 +14,7 @@ import LocalAuth import PasscodeUI import TelegramStringFormatting import TelegramIntents +import ContextUI private final class PasscodeOptionsControllerArguments { let turnPasscodeOff: () -> Void @@ -153,6 +154,18 @@ private struct PasscodeOptionsControllerState: Equatable { } } +private final class PasscodeOptionsContextReferenceContentSource: 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)) + } +} + private struct PasscodeOptionsData: Equatable { let accessChallenge: PostboxAccessChallengeData let presentationSettings: PresentationPasscodeSettings @@ -226,9 +239,12 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode let statePromise = ValuePromise(initialState, ignoreRepeated: true) var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var popControllerImpl: (() -> Void)? var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? + var findAutolockReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentAutolockTimeout: Int32? let actionsDisposable = DisposableSet() @@ -323,8 +339,6 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode }) }, changePasscodeTimeout: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] let setAction: (Int32?) -> Void = { value in let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in @@ -342,24 +356,38 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode values.sort() #endif + var items: [ContextMenuItem] = [] for value in values { var t: Int32? if value != 0 { t = value } - items.append(ActionSheetButtonItem(title: autolockStringForTimeout(strings: presentationData.strings, timeout: t), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - + items.append(.action(ContextMenuActionItem(text: autolockStringForTimeout(strings: presentationData.strings, timeout: t), icon: { theme in + if currentAutolockTimeout == t { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) setAction(t) - })) + }))) } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + guard let sourceNode = findAutolockReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(PasscodeOptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, changeTouchId: { value in let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in passcodeOptionsDataPromise?.set(.single(data.withUpdatedPresentationSettings(data.presentationSettings.withUpdatedEnableBiometrics(value)))) @@ -372,7 +400,8 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue |> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in - + currentAutolockTimeout = passcodeOptionsData.presentationSettings.autolockTimeout + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) @@ -387,6 +416,9 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } @@ -396,6 +428,9 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode replaceTopControllerImpl = { [weak controller] c, animated in (controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated) } + findAutolockReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: PasscodeOptionsEntryTag.autolock) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index c7bf85fd4a..4ed39ba4d5 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -21,6 +21,7 @@ import AuthorizationUI import AuthenticationServices import ChatTimerScreen import PasskeysScreen +import ContextUI private final class PrivacyAndSecurityControllerArguments { let account: Account @@ -560,6 +561,18 @@ private struct PrivacyAndSecurityControllerState: Equatable { var updatingOnlyAllowPremiumNonContacts: Bool? = nil } +private final class PrivacyAndSecurityContextReferenceContentSource: 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)) + } +} + private func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> Int { var result = 0 for (_, peer) in peers { @@ -860,7 +873,9 @@ public func privacyAndSecurityController( var pushControllerImpl: ((ViewController, Bool) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var getNavigationControllerImpl: (() -> NavigationController?)? + var findAccountTimeoutReferenceNode: (() -> ItemListDisclosureItemNode?)? let actionsDisposable = DisposableSet() @@ -1335,12 +1350,8 @@ public func privacyAndSecurityController( |> take(1) |> deliverOnMainQueue updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in - if let _ = privacySettingsValue { + if let privacySettingsValue = privacySettingsValue { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let timeoutAction: (Int32) -> Void = { timeout in if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable { updateState { state in @@ -1377,15 +1388,24 @@ public func privacyAndSecurityController( 548 * 24 * 60 * 60, 730 * 24 * 60 * 60 ] - var timeoutItems: [ActionSheetItem] = timeoutValues.map { value in - return ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))), action: { - dismissAction() + var timeoutItems: [ContextMenuItem] = timeoutValues.map { value in + return .action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))), icon: { theme in + if privacySettingsValue.accountRemovalTimeout == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) timeoutAction(value) - }) + })) } - timeoutItems.append(ActionSheetButtonItem(title: presentationData.strings.PrivacySettings_DeleteAccountNow, color: .destructive, action: { - dismissAction() - + timeoutItems.append(.separator) + timeoutItems.append(.action(ContextMenuActionItem(text: presentationData.strings.PrivacySettings_DeleteAccountNow, textColor: .destructive, icon: { _ in + return nil + }, action: { _, f in + f(.default) + guard let navigationController = getNavigationControllerImpl?() else { return } @@ -1396,12 +1416,22 @@ public func privacyAndSecurityController( let optionsController = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) pushControllerImpl?(optionsController, true) }) - })) - controller.setItemGroups([ - ActionSheetItemGroup(items: timeoutItems), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller) + }))) + + guard let sourceNode = findAccountTimeoutReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(PrivacyAndSecurityContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(timeoutItems))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) } })) }, setupMessageAutoremove: { @@ -1623,9 +1653,15 @@ public func privacyAndSecurityController( presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root), with: nil) } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } getNavigationControllerImpl = { [weak controller] in return (controller?.navigationController as? NavigationController) } + findAccountTimeoutReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: PrivacyAndSecurityEntryTag.accountTimeout) as? ItemListDisclosureItemNode + } controller.didAppear = { _ in updateHasTwoStepAuth() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift index e8847e7d57..96e61a27ee 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift @@ -166,7 +166,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode { let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) let buttonInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { buttonInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { buttonInset = 0.0 diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift index 6cb6df3afe..178e50d150 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift @@ -265,19 +265,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) - let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) - - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 14.0 - case .legacy: - verticalInset = 10.0 - } + let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 1.0 - let textSpacing: CGFloat = 3.0 + let textSpacing: CGFloat = 2.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme @@ -307,13 +300,6 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { deviceString = item.session.deviceModel } -// if !item.session.platform.isEmpty { -// if !deviceString.isEmpty { -// deviceString += ", " -// } -// deviceString += item.session.platform -// } - var updatedIcon: UIImage? if item.session != currentItem?.session { updatedIcon = iconForSession(item.session).0 @@ -352,7 +338,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) let separatorHeight = UIScreenPixel let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 @@ -516,8 +502,8 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 12.0), size: CGSize(width: 30.0, height: 30.0))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset - 2.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index 4015f7e7de..0d75346ebb 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -214,19 +214,12 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) - let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) - - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 14.0 - case .legacy: - verticalInset = 10.0 - } + let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 1.0 - let textSpacing: CGFloat = 3.0 + let textSpacing: CGFloat = 2.0 if currentItem?.presentationData !== item.presentationData.theme { updatedTheme = item.presentationData.theme @@ -288,7 +281,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) let separatorHeight = UIScreenPixel let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 @@ -447,9 +440,9 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 12.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset - 2.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size)) @@ -557,8 +550,6 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } } -private let connectedBotAvatarFont = avatarPlaceholderFont(size: 11.0) - final class ItemListConnectedBotSessionItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData @@ -690,7 +681,7 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt self.containerNode = ASDisplayNode() - self.avatarNode = AvatarNode(font: connectedBotAvatarFont) + self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.cornerRadius = 7.0 self.avatarNode.clipsToBounds = true @@ -736,19 +727,12 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt return { item, params, neighbors in var updatedTheme: PresentationTheme? - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) - let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 14.0 - case .legacy: - verticalInset = 10.0 - } - + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 1.0 - let textSpacing: CGFloat = 3.0 + let textSpacing: CGFloat = 2.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme @@ -780,7 +764,7 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) let separatorHeight = UIScreenPixel let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 @@ -901,8 +885,8 @@ private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsIt transition.updateFrame(node: self.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: self.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 12.0), size: CGSize(width: 30.0, height: 30.0))) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset - 2.0), size: titleLayout.size)) transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.appNode.frame.maxY + textSpacing), size: locationLayout.size)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 1b7ef6be58..a177304ec6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -15,6 +15,7 @@ import ComponentFlow import AlertComponent import AlertCheckComponent import AppBundle +import ContextUI private final class RecentSessionsControllerArguments { let context: AccountContext @@ -615,6 +616,18 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController { } +private final class RecentSessionsContextReferenceContentSource: 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)) + } +} + public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool, focusOnItemTag: RecentSessionsEntryTag? = nil) -> ViewController & RecentSessionsController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) @@ -632,8 +645,11 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont webSessionsContext.loadMore() var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? + var findAutoTerminateReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentAuthorizationTTLDays: Int32? let actionsDisposable = DisposableSet() @@ -894,10 +910,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/apps", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }, setupAuthorizationTTL: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let ttlAction: (Int32) -> Void = { ttl in updateAuthorizationTTLDisposable.set(activeSessionsContext.updateAuthorizationTTL(days: ttl).start()) } @@ -907,17 +919,32 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont 90, 180 ] - let timeoutItems: [ActionSheetItem] = timeoutValues.map { value in - return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value * 24 * 60 * 60), action: { - dismissAction() + let timeoutItems: [ContextMenuItem] = timeoutValues.map { value in + return .action(ContextMenuActionItem(text: timeIntervalString(strings: presentationData.strings, value: value * 24 * 60 * 60), icon: { theme in + if currentAuthorizationTTLDays == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) ttlAction(value) - }) + })) } - controller.setItemGroups([ - ActionSheetItemGroup(items: timeoutItems), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, nil) + guard let sourceNode = findAutoTerminateReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(RecentSessionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(timeoutItems))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, openDesktopLink: { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }, openWebLink: { @@ -959,6 +986,8 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin, connectedBotAndPeer) |> deliverOnMainQueue |> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin, connectedBotAndPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in + currentAuthorizationTTLDays = sessionsState.ttlDays + var rightNavigationButton: ItemListNavigationButton? let websites = websitesAndPeers.sessions let peers = websitesAndPeers.peers @@ -982,11 +1011,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } let emptyStateItem: ItemListControllerEmptyStateItem? = nil -// if sessionsState.sessions.count == 1 && mode == .sessions { -// emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings) -// } else { -// emptyStateItem = nil -// } let title: ItemListControllerTitle let entries: [RecentSessionsEntry] @@ -1032,12 +1056,18 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } pushControllerImpl = { [weak controller] c in controller?.push(c) } dismissImpl = { [weak controller] in controller?.dismiss() } + findAutoTerminateReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: RecentSessionsEntryTag.autoTerminate) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index 0b9dab7e70..2b6d4b53cd 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -72,7 +72,7 @@ class RecentSessionsHeaderItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(13.0) +private let titleFont = Font.regular(14.0) class RecentSessionsHeaderItemNode: ListViewItemNode { private let titleNode: TextNode @@ -129,11 +129,11 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { updatedTheme = item.theme } - let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in + let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) })) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.2, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + 69.0) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -158,7 +158,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme)) } - let buttonSideInset: CGFloat = 36.0 + let buttonSideInset: CGFloat = 16.0 let buttonWidth = min(375, contentSize.width - buttonSideInset * 2.0) let buttonHeight = 52.0 let buttonFrame = CGRect(x: floorToScreenPixels((params.width - buttonWidth) / 2.0), y: contentSize.height - buttonHeight + 4.0, width: buttonWidth, height: buttonHeight) diff --git a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift index 116728780b..1727e136ba 100644 --- a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift +++ b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift @@ -15,6 +15,7 @@ import PresentationDataUtils import TelegramNotices import NotificationSoundSelectionUI import TelegramStringFormatting +import ContextUI private final class ReactionNotificationSettingsControllerArguments { let context: AccountContext @@ -270,12 +271,26 @@ private struct ReactionNotificationSettingsState: Equatable { } } +private final class ReactionNotificationSettingsContextReferenceContentSource: 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)) + } +} + public func reactionNotificationSettingsController( context: AccountContext, focusOnItemTag: ReactionNotificationSettingsEntryTag? = nil ) -> ViewController { - var presentControllerImpl: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? + var findCategoryReferenceNode: ((ReactionNotificationSettingsEntryTag) -> ItemListSwitchItemNode?)? + var currentReactionSettings = PeerReactionNotificationSettings.default let stateValue = Atomic(value: ReactionNotificationSettingsState()) let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) @@ -290,49 +305,48 @@ public func reactionNotificationSettingsController( let openCategory: (Bool) -> Void = { isMessages in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let text: String - if isMessages { - text = presentationData.strings.Notifications_Reactions_SheetTitleMessages - } else { - text = presentationData.strings.Notifications_Reactions_SheetTitleStories + + let currentValue: PeerReactionNotificationSettings.Sources = isMessages ? currentReactionSettings.messages : currentReactionSettings.stories + let options: [(PeerReactionNotificationSettings.Sources, String)] = [ + (.everyone, presentationData.strings.Notifications_Reactions_SheetValueEveryone), + (.contacts, presentationData.strings.Notifications_Reactions_SheetValueContacts) + ] + let items: [ContextMenuItem] = options.map { value, title in + return .action(ContextMenuActionItem(text: title, icon: { theme in + if currentValue == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + if isMessages { + settings.reactionSettings.messages = value + } else { + settings.reactionSettings.stories = value + } + return settings + }).start() + })) } - - let actionSheet = ActionSheetController(presentationData: presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: text), - ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueEveryone, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - if isMessages { - settings.reactionSettings.messages = .everyone - } else { - settings.reactionSettings.stories = .everyone - } - return settings - }).start() - }), - ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueContacts, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - if isMessages { - settings.reactionSettings.messages = .contacts - } else { - settings.reactionSettings.stories = .contacts - } - return settings - }).start() - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) + + let sourceTag: ReactionNotificationSettingsEntryTag = isMessages ? .messages : .stories + guard let sourceNode = findCategoryReferenceNode?(sourceTag) else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(ReactionNotificationSettingsContextReferenceContentSource(sourceView: sourceNode.contextSourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) } let arguments = ReactionNotificationSettingsControllerArguments( @@ -399,6 +413,7 @@ public func reactionNotificationSettingsController( } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings } + currentReactionSettings = viewSettings.reactionSettings let entries = reactionNotificationSettingsEntries( globalSettings: viewSettings, @@ -422,12 +437,15 @@ public func reactionNotificationSettingsController( } let controller = ItemListController(context: context, state: signal) - presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } + findCategoryReferenceNode = { [weak controller] tag in + return controller?.itemNode(forTag: tag) as? ItemListSwitchItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 13c028dd64..b30559b2a7 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -13,6 +13,7 @@ import StickerPackPreviewUI import ItemListStickerPackItem import ItemListPeerActionItem import UndoUI +import ContextUI import WebPBinding import ReactionImageComponent @@ -507,6 +508,18 @@ private struct InstalledStickerPacksControllerState: Equatable { } } +private final class InstalledStickerPacksSuggestOptionsContextReferenceContentSource: 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)) + } +} + private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> EngineItemCollectionId.Namespace { switch mode { case .general, .modal: @@ -662,9 +675,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var navigateToChatControllerImpl: ((EnginePeer.Id) -> Void)? var dismissImpl: (() -> Void)? + var findSuggestOptionsReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentEmojiStickerSuggestionMode = StickerSettings.defaultSettings.emojiStickerSuggestionMode let actionsDisposable = DisposableSet() @@ -801,30 +817,41 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta updatedPacks(packs) })) }, openSuggestOptions: { - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let options: [(EmojiStickerSuggestionMode, String)] = [ (.all, presentationData.strings.Stickers_SuggestAll), (.installed, presentationData.strings.Stickers_SuggestAdded), (.none, presentationData.strings.Stickers_SuggestNone) ] - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: presentationData.strings.Stickers_SuggestStickers)) + var items: [ContextMenuItem] = [] for (option, title) in options { - items.append(ActionSheetButtonItem(title: title, color: .accent, action: { - dismissAction() + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + if currentEmojiStickerSuggestionMode == option { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) let _ = updateStickerSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedEmojiStickerSuggestionMode(option) }).start() - })) + }))) } - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + + guard let sourceNode = findSuggestOptionsReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(InstalledStickerPacksSuggestOptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, toggleSuggestAnimatedEmoji: { value in let _ = updateStickerSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedSuggestAnimatedEmoji(value) @@ -938,6 +965,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value } + currentEmojiStickerSuggestionMode = stickerSettings.emojiStickerSuggestionMode var packCount: Int? = nil var stickerPacks: [EngineRawItemCollectionInfoEntry] = [] @@ -1213,6 +1241,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } presentStickerPackController = { [weak controller] info in let _ = (stickerPacks.get() |> take(1) @@ -1298,6 +1329,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta dismissImpl = { [weak controller] in controller?.dismiss() } + findSuggestOptionsReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: InstalledStickerPacksEntryTag.suggestOptions) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift index 6243b349b6..6855051a2a 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift @@ -199,6 +199,12 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { deinit { } + override func didLoad() { + super.didLoad() + + self.scrollNode.view.scrollsToTop = false + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index edf1045e71..540b821cf4 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -188,6 +188,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.pageControlNode.setPage(0.0) } diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index 92b182dd31..4b0d52dd75 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -80,7 +80,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = true - self.imageNode.cornerRadius = 8.0 + self.imageNode.cornerRadius = 16.0 self.imageNode.clipsToBounds = true self.overlayNode = ASImageNode() @@ -153,11 +153,6 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { } } -// override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { -// let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) -// self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) -// } - func setup(item: ThemeCarouselThemeIconItem, size: CGSize) { let currentItem = self.item let currentSize = self.size @@ -417,6 +412,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + let padding: CGFloat = 12.0 let minSpacing: CGFloat = 6.0 let referenceImageSize: CGSize @@ -428,18 +424,13 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { } let totalWidth = params.width - params.leftInset - params.rightInset let imageCount = Int((totalWidth - minSpacing) / (referenceImageSize.width + minSpacing)) - var itemSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((totalWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + var itemSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((totalWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) itemSize.height = referenceImageSize.height - let itemSpacing = floorToScreenPixels((totalWidth - CGFloat(imageCount) * itemSize.width) / CGFloat(imageCount + 1)) - - var spacingOffset: CGFloat = 0.0 - if totalWidth - CGFloat(imageCount) * itemSize.width - CGFloat(imageCount + 1) * itemSpacing == 1.0 { - spacingOffset = UIScreenPixel - } - + let itemSpacing = floorToScreenPixels((totalWidth - padding * 2.0 - CGFloat(imageCount) * itemSize.width) / CGFloat(imageCount - 1)) + let rows = ceil(CGFloat(item.themes.count) / CGFloat(imageCount)) - contentSize = CGSize(width: params.width, height: minSpacing + rows * (itemSize.height + itemSpacing)) + contentSize = CGSize(width: params.width, height: padding * 2.0 + rows * itemSize.height + (rows - 1.0) * itemSpacing) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -450,6 +441,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.item = item strongSelf.layoutParams = params + strongSelf.scrollNode.view.scrollsToTop = false strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -492,7 +484,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -525,7 +517,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { let col = CGFloat(index % imageCount) let row = floor(CGFloat(index) / CGFloat(imageCount)) - let itemFrame = CGRect(origin: CGPoint(x: params.leftInset + spacingOffset + itemSpacing + (itemSize.width + itemSpacing) * col, y: minSpacing + (itemSize.height + itemSpacing) * row), size: itemSize) + let itemFrame = CGRect(origin: CGPoint(x: params.leftInset + padding + (itemSize.width + itemSpacing) * col, y: padding + (itemSize.height + itemSpacing) * row), size: itemSize) itemNode.frame = itemFrame index += 1 diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 80bcb63874..077a07ec0c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -59,7 +59,9 @@ public final class ThemePreviewController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationTheme.set(.single(previewTheme)) - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings, style: .glass)) + + self._hasGlassStyle = true self.blocksBackgroundWhenInOverlay = true self.acceptsFocusWhenInOverlay = true @@ -140,13 +142,13 @@ public final class ThemePreviewController: ViewController { let titleView = CounterControllerTitleView(theme: self.previewTheme) titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? " " : "") self.navigationItem.titleView = titleView - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.statusBar.statusBarStyle = self.previewTheme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) if !isPreview { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Navigation/Share"), color: self.previewTheme.chat.inputPanel.panelControlColor), style: .plain, target: self, action: #selector(self.actionPressed)) } self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get()) @@ -155,7 +157,7 @@ public final class ThemePreviewController: ViewController { let titleView = CounterControllerTitleView(theme: strongSelf.previewTheme) titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "") strongSelf.navigationItem.titleView = titleView - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings), transition: .immediate) + strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings, style: .glass), transition: .immediate) } }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index d6650d4e69..bbad3bbf56 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -298,7 +298,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { } strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))() - strongSelf.toolbarNode.dark = useDarkButton + strongSelf.toolbarNode.dark = false + let _ = useDarkButton } }) } @@ -318,6 +319,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.pageControlNode.setPage(0.0) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift index fad4ad8e12..17c4abb873 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift @@ -842,7 +842,7 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 8140064aff..3db095dc8c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -179,7 +179,7 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize) self.overlayNode.frame = self.iconNode.frame - let textSize = self.textNode.updateLayout(bounds.size) + let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width + 8.0, height: bounds.size.height)) let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize) self.textNode.frame = textFrame diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 492089f2e9..7566da5364 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -456,6 +456,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() if self.fromForeignApp { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index dc2f656c36..5fbf1ed03e 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -22,9 +22,9 @@ final class StickerPackPreviewInteraction { let removeStickerPack: (StickerPackCollectionInfo) -> Void let emojiSelected: (String, ChatTextInputTextCustomEmojiAttribute) -> Void let emojiLongPressed: (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void - let addPressed: () -> Void + let addPressed: (UIView) -> Void - init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping () -> Void) { + init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping (UIView) -> Void) { self.playAnimatedStickers = playAnimatedStickers self.addStickerPack = addStickerPack self.removeStickerPack = removeStickerPack @@ -128,7 +128,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.containerNode = ASDisplayNode() self.imageNode = TransformImageNode() - self.imageNode.isLayerBacked = !smartInvertColorsEnabled() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false @@ -194,7 +193,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } @objc private func handleAddTap() { - self.interaction?.addPressed() + self.interaction?.addPressed(self.imageNode.view) } private var setupTimestamp: Double? diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 4a968b8c6f..8aeb09cc88 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -269,7 +269,7 @@ private final class StickerPackContainer: ASDisplayNode { var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? var emojiSelectedImpl: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? var emojiLongPressedImpl: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? - var addPressedImpl: (() -> Void)? + var addPressedImpl: ((UIView) -> Void)? self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true, addStickerPack: { info, items in addStickerPackImpl?(info, items) }, removeStickerPack: { info in @@ -278,8 +278,8 @@ private final class StickerPackContainer: ASDisplayNode { emojiSelectedImpl?(text, attribute) }, emojiLongPressed: { text, attribute, node, frame in emojiLongPressedImpl?(text, attribute, node, frame) - }, addPressed: { - addPressedImpl?() + }, addPressed: { sourceView in + addPressedImpl?(sourceView) }) super.init() @@ -326,33 +326,39 @@ private final class StickerPackContainer: ASDisplayNode { return targetOffset } - let insets = strongSelf.gridNode.scrollView.contentInset + guard let (layout, _, titleAreaInset, gridInsets) = strongSelf.validLayout else { + return targetOffset + } + + let collapsedOffset = -gridInsets.top + let expandedOffset = strongSelf.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) var modalProgress: CGFloat = 0.0 var updatedOffset = targetOffset var resetOffset = false - if targetOffset.y < 0.0 && targetOffset.y >= -insets.top { - if contentOffset.y > 0.0 { - updatedOffset = CGPoint(x: 0.0, y: 0.0) + if targetOffset.y < expandedOffset && targetOffset.y >= collapsedOffset { + if contentOffset.y > expandedOffset { + updatedOffset = CGPoint(x: 0.0, y: expandedOffset) modalProgress = 1.0 } else { - if targetOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { + let middleOffset = collapsedOffset + (expandedOffset - collapsedOffset) / 2.0 + if targetOffset.y > middleOffset || velocity.y <= -100.0 { modalProgress = 1.0 resetOffset = true } else { modalProgress = 0.0 - if contentOffset.y > -insets.top { + if contentOffset.y > collapsedOffset { resetOffset = true } } } - } else if targetOffset.y >= 0.0 { + } else if targetOffset.y >= expandedOffset { modalProgress = 1.0 } if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { - if contentOffset.y > 0.0 && targetOffset.y > 0.0 { + if contentOffset.y > expandedOffset && targetOffset.y > expandedOffset { } else { resetOffset = true } @@ -364,10 +370,10 @@ private final class StickerPackContainer: ASDisplayNode { let offset: CGPoint let isVelocityAligned: Bool if modalProgress.isZero { - offset = CGPoint(x: 0.0, y: -insets.top) + offset = CGPoint(x: 0.0, y: collapsedOffset) isVelocityAligned = velocity.y < 0.0 } else { - offset = CGPoint(x: 0.0, y: 0.0) + offset = CGPoint(x: 0.0, y: expandedOffset) isVelocityAligned = velocity.y > 0.0 } @@ -459,8 +465,8 @@ private final class StickerPackContainer: ASDisplayNode { longPressEmoji?(text, attribute, node, frame) } - addPressedImpl = { [weak self] in - self?.presentAddStickerOptions() + addPressedImpl = { [weak self] sourceView in + self?.presentAddStickerOptions(sourceView: sourceView) } } @@ -1209,33 +1215,44 @@ private final class StickerPackContainer: ASDisplayNode { } private let stickerPickerInputData = Promise() - private func presentAddStickerOptions() { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.StickerPack_CreateNew, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - + private func presentAddStickerOptions(sourceView: UIView) { + guard let controller = self.controller else { + return + } + + let presentationData = self.presentationData + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_CreateNew, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + guard let self, let controller = self.controller else { return } self.presentCreateSticker() controller.controllerNode.dismiss() - })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.StickerPack_AddExisting, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_AddExisting, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) guard let self, let controller = self.controller else { return } self.presentAddExistingSticker() controller.controllerNode.dismiss() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.presentInGlobalOverlay(actionSheet, nil) + }))) + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: nil + ) + self.presentInGlobalOverlay(contextController, nil) let stickerItems = EmojiPagerContentComponent.stickerInputData( context: self.context, @@ -1598,8 +1615,8 @@ private final class StickerPackContainer: ASDisplayNode { self.onReady() if !contents.isEmpty && self.currentStickerPacks.isEmpty { - if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) + if let (layout, _, titleAreaInset, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top(self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets)), transition: .immediate, directionHint: .up, adjustForSection: false) } } @@ -1671,8 +1688,8 @@ private final class StickerPackContainer: ASDisplayNode { let info = info._parse() if !items.isEmpty && self.currentStickerPack == nil { - if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) + if let (layout, _, titleAreaInset, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top(self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets)), transition: .immediate, directionHint: .up, adjustForSection: false) } } @@ -1875,12 +1892,37 @@ private final class StickerPackContainer: ASDisplayNode { return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset) } + private func expandedContentOffset(layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + if case .regular = layout.metrics.widthClass { + return 0.0 + } else { + return max(-gridInsets.top, -titleAreaInset) + } + } + + private func expandProgress(contentOffset: CGFloat, layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + let collapsedOffset = -gridInsets.top + let expandedOffset = self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let distance = expandedOffset - collapsedOffset + if distance <= CGFloat.ulpOfOne { + return 1.0 + } + return max(0.0, min(1.0, (contentOffset - collapsedOffset) / distance)) + } + + private func contentOffsetForExpandProgress(_ expandProgress: CGFloat, layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + let collapsedOffset = -gridInsets.top + let expandedOffset = self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let progress = max(0.0, min(1.0, expandProgress)) + return collapsedOffset + (expandedOffset - collapsedOffset) * progress + } + func syncExpandProgress(expandScrollProgress: CGFloat, expandProgress: CGFloat, modalProgress: CGFloat, transition: ContainedViewLayoutTransition) { - guard let (_, _, _, gridInsets) = self.validLayout else { + guard let (layout, _, titleAreaInset, gridInsets) = self.validLayout else { return } - let contentOffset = (1.0 - expandScrollProgress) * (-gridInsets.top) + let contentOffset = self.contentOffsetForExpandProgress(expandScrollProgress, layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) if case let .animated(duration, _) = transition { self.gridNode.autoscroll(toOffset: CGPoint(x: 0.0, y: contentOffset), duration: duration) } else { @@ -2152,11 +2194,11 @@ private final class StickerPackContainer: ASDisplayNode { let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 90.0)) transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) - self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.9, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0)) transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) - self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.9, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) if firstTime { while !self.enqueuedTransactions.isEmpty { @@ -2178,10 +2220,8 @@ private final class StickerPackContainer: ASDisplayNode { let minBackgroundY = gridFrame.minY let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset - let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top - let expandHeight: CGFloat = 100.0 - let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) - let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top))) + let expandProgress = self.expandProgress(contentOffset: presentationLayout.contentOffset.y, layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let expandScrollProgress = expandProgress let modalProgress = max(0.0, min(1.0, expandScrollProgress)) let expandProgressTransition = transition diff --git a/submodules/TelegramApi/Sources/Api41.swift b/submodules/TelegramApi/Sources/Api41.swift index 0554a13692..4f6a76437f 100644 --- a/submodules/TelegramApi/Sources/Api41.swift +++ b/submodules/TelegramApi/Sources/Api41.swift @@ -1644,19 +1644,19 @@ public extension Api.functions.account { } } public extension Api.functions.account { - static func toggleWebBrowserSettingsException(flags: Int32, openExternalBrowser: Api.Bool?, url: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func toggleWebBrowserSettingsException(flags: Int32, openExternalBrowser: Api.Bool?, url: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(755631473) + buffer.appendInt32(1626161705) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 { openExternalBrowser!.serialize(buffer, true) } serializeString(url, buffer: buffer, boxed: false) - return (FunctionDescription(name: "account.toggleWebBrowserSettingsException", parameters: [("flags", ConstructorParameterDescription(flags)), ("openExternalBrowser", ConstructorParameterDescription(openExternalBrowser)), ("url", ConstructorParameterDescription(url))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "account.toggleWebBrowserSettingsException", parameters: [("flags", ConstructorParameterDescription(flags)), ("openExternalBrowser", ConstructorParameterDescription(openExternalBrowser)), ("url", ConstructorParameterDescription(url))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.Updates? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.Updates } return result }) @@ -4922,12 +4922,13 @@ public extension Api.functions.contacts { } } public extension Api.functions.contacts { - static func search(q: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func search(flags: Int32, q: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(301470424) + buffer.appendInt32(99978511) + serializeInt32(flags, buffer: buffer, boxed: false) serializeString(q, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "contacts.search", parameters: [("q", ConstructorParameterDescription(q)), ("limit", ConstructorParameterDescription(limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.contacts.Found? in + return (FunctionDescription(name: "contacts.search", parameters: [("flags", ConstructorParameterDescription(flags)), ("q", ConstructorParameterDescription(q)), ("limit", ConstructorParameterDescription(limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.contacts.Found? in let reader = BufferReader(buffer) var result: Api.contacts.Found? if let signature = reader.readInt32() { diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index e76f43d4a4..1ffa22114d 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -352,6 +352,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 58b4bd3d1a..f6df75ee72 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -313,34 +313,34 @@ public final class CallController: ViewController { } } - self.controllerNode.callEnded = { [weak self] didPresentRating in - if let strongSelf = self, !didPresentRating { - let _ = (combineLatest(strongSelf.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]), ApplicationSpecificNotice.getCallsTabTip(accountManager: strongSelf.sharedContext.accountManager)) - |> map { sharedData, callsTabTip -> Int32 in - var value = false - if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) { - value = settings.showTab - } - if value { - return 3 - } else { - return callsTabTip - } - } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in - if let strongSelf = self { - if callsTabTip == 2 { - Queue.mainQueue().after(1.0) { - let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) - strongSelf.present(controller, in: .window(.root)) - } - } - if callsTabTip < 3 { - let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).start() - } - } - }) - } - } +// self.controllerNode.callEnded = { [weak self] didPresentRating in +// if let strongSelf = self, !didPresentRating { +// let _ = (combineLatest(strongSelf.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]), ApplicationSpecificNotice.getCallsTabTip(accountManager: strongSelf.sharedContext.accountManager)) +// |> map { sharedData, callsTabTip -> Int32 in +// var value = false +// if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) { +// value = settings.showTab +// } +// if value { +// return 3 +// } else { +// return callsTabTip +// } +// } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in +// if let strongSelf = self { +// if callsTabTip == 2 { +// Queue.mainQueue().after(1.0) { +// let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) +// strongSelf.present(controller, in: .window(.root)) +// } +// } +// if callsTabTip < 3 { +// let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).start() +// } +// } +// }) +// } +// } self.controllerNode.willBeDismissedInteractively = { [weak self] in guard let self else { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index f950e540f0..b0adada425 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -158,6 +158,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, A self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index a158d695d3..ec5b336544 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -241,6 +241,7 @@ public final class VoiceChatJoinScreen: ViewController { self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift index 8d61502466..0e5c02ff8e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift @@ -158,6 +158,7 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift b/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift index 35eda67624..52a6303502 100644 --- a/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift @@ -176,16 +176,46 @@ public func toggleWebBrowserSettingsException(postbox: Postbox, network: Network guard let result else { return .single(false) } - switch result { - case .boolTrue: - return Signal { subscriber in - return _internal_getAccountWebBrowserSettings(postbox: postbox, network: network, forceUpdate: true).start(completed: { - subscriber.putNext(true) - subscriber.putCompletion() + + return postbox.transaction { transaction -> Bool in + var settings = transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + var updated = false + for update in result.allUpdates { + switch update { + case let .updateWebBrowserSettings(updateWebBrowserSettingsData): + settings = settings.withUpdatedOpenExternalBrowser((updateWebBrowserSettingsData.flags & (1 << 0)) != 0) + updated = true + case let .updateWebBrowserException(updateWebBrowserExceptionData): + let openExternalBrowser: Bool? + if let value = updateWebBrowserExceptionData.openExternalBrowser { + switch value { + case .boolFalse: + openExternalBrowser = false + case .boolTrue: + openExternalBrowser = true + } + } else { + openExternalBrowser = nil + } + + settings = settings.withAppliedExceptionUpdate( + openExternalBrowser: openExternalBrowser, + delete: (updateWebBrowserExceptionData.flags & (1 << 1)) != 0, + exception: AccountWebBrowserException(apiWebDomainException: updateWebBrowserExceptionData.exception) + ) + updated = true + default: + break + } + } + + if updated { + transaction.updatePreferencesEntry(key: PreferencesKeys.webBrowserSettings, { _ in + return PreferencesEntry(settings) }) } - case .boolFalse: - return .single(false) + + return updated } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 28543c3ccf..0c253227a2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -60,6 +60,9 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } + if (flags & (1 << 3)) != 0 { + resultFlags.insert(.sameOrigin) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } @@ -111,6 +114,9 @@ func _internal_requestMainWebView(postbox: Postbox, network: Network, peerId: Pe if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } + if (flags & (1 << 3)) != 0 { + resultFlags.insert(.sameOrigin) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } @@ -137,6 +143,7 @@ public struct RequestWebViewResult { public static let fullSize = Flags(rawValue: 1 << 0) public static let fullScreen = Flags(rawValue: 1 << 1) + public static let sameOrigin = Flags(rawValue: 1 << 2) } public let flags: Flags @@ -270,6 +277,9 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: if (webViewFlags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } + if (webViewFlags & (1 << 3)) != 0 { + resultFlags.insert(.sameOrigin) + } let keepAlive: Signal? if let queryId { keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, monoforumPeerId: monoforumPeerId, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil) @@ -363,6 +373,9 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage if (flags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } + if (flags & (1 << 3)) != 0 { + resultFlags.insert(.sameOrigin) + } return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil)) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index e82a0a009c..30e8a3c0e4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -27,7 +27,7 @@ public enum TelegramSearchPeersScope: Equatable { } public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, network: Network, query: String, scope: TelegramSearchPeersScope) -> Signal<([FoundPeer], [FoundPeer]), NoError> { - let searchResult = network.request(Api.functions.contacts.search(q: query, limit: 20), automaticFloodWait: false) + let searchResult = network.request(Api.functions.contacts.search(flags: 0, q: query, limit: 20), automaticFloodWait: false) |> map(Optional.init) |> `catch` { _ in return Signal.single(nil) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index 325e48edfb..eec3004928 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -45,7 +45,7 @@ public struct PresentationResourcesRootController { public static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.rootController.navigationBar.buttonColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlColor) }) // return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 15cc9e0b4b..c90687cf93 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1924,8 +1924,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome { if let value = dice.value { + let isAccountPeerOutcome: Bool + if let forwardInfo = message.forwardInfo { + isAccountPeerOutcome = forwardInfo.author?.id == accountPeerId + } else { + isAccountPeerOutcome = message.author?.id == accountPeerId + } + let rawString: String - if message.author?.id == accountPeerId { + if isAccountPeerOutcome { if value == 1, let tonAmount = dice.tonAmount { let value = formatTonAmountText(tonAmount, dateTimeFormat: dateTimeFormat) rawString = strings.Conversation_EmojiStake_LostYou(value).string diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 4d0abab7f1..49975963fd 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -170,7 +170,6 @@ swift_library( "//submodules/YuvConversion:YuvConversion", "//submodules/JoinLinkPreviewUI:JoinLinkPreviewUI", "//submodules/LanguageLinkPreviewUI:LanguageLinkPreviewUI", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", "//submodules/MimeTypes:MimeTypes", "//submodules/LocalMediaResources:LocalMediaResources", diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift index 3ef1e0ae7f..948f5eac49 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift @@ -135,7 +135,7 @@ public final class AlertWebpagePreviewComponent: Component { clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, - sendMessage: { _ in }, + sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, @@ -148,8 +148,8 @@ public final class AlertWebpagePreviewComponent: Component { activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in }, - shareCurrentLocation: {}, - shareAccountContact: {}, + shareCurrentLocation: { _ in }, + shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, @@ -200,7 +200,7 @@ public final class AlertWebpagePreviewComponent: Component { displaySwipeToReplyHint: {}, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, - openPollCreation: { _ in }, + openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in }, diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index 02494198e8..23f1203c65 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -836,7 +836,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon insets.bottom += 60.0 let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 2b28a3c333..b62e67d0e4 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -344,13 +344,31 @@ public final class ButtonComponent: Component { case actualGlass case legacy } - + + public struct Gradient: Equatable { + public enum Animation: Equatable { + case horizontalShift(duration: Double) + } + + public var colors: [UIColor] + public var animation: Animation + + public init( + colors: [UIColor], + animation: Animation = .horizontalShift(duration: 4.5) + ) { + self.colors = colors + self.animation = animation + } + } + public var style: Style public var color: UIColor public var foreground: UIColor public var pressedColor: UIColor public var cornerRadius: CGFloat public var isShimmering: Bool + public var gradient: Gradient? public init( style: Style = .legacy, @@ -358,7 +376,8 @@ public final class ButtonComponent: Component { foreground: UIColor, pressedColor: UIColor, cornerRadius: CGFloat = 10.0, - isShimmering: Bool = false + isShimmering: Bool = false, + gradient: Gradient? = nil ) { self.style = style self.color = color @@ -366,6 +385,7 @@ public final class ButtonComponent: Component { self.pressedColor = pressedColor self.cornerRadius = cornerRadius self.isShimmering = isShimmering + self.gradient = gradient } public func withIsShimmering(_ isShimmering: Bool) -> Background { @@ -375,7 +395,8 @@ public final class ButtonComponent: Component { foreground: self.foreground, pressedColor: self.pressedColor, cornerRadius: self.cornerRadius, - isShimmering: isShimmering + isShimmering: isShimmering, + gradient: self.gradient ) } } @@ -468,6 +489,7 @@ public final class ButtonComponent: Component { private let glassHighlightRecognizer: GlassHighlightGestureRecognizer private var shimmeringView: ButtonShimmeringView? + private var gradientBackgroundView: AnimatedGradientBackgroundView? private var chromeView: UIImageView? private var contentItem: ContentItem? @@ -584,11 +606,49 @@ public final class ButtonComponent: Component { } transition.setFrame(view: glassHighlightContainerView, frame: CGRect(origin: .zero, size: size)) transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius) - + self.glassHighlightRecognizer.highlightContainerView = glassHighlightContainerView self.glassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress } - + + private func updateGradientBackground(component: ButtonComponent, contentContainerView: UIView, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + guard component.background.style != .actualGlass, let gradient = component.background.gradient, gradient.colors.count > 1 else { + if let gradientBackgroundView = self.gradientBackgroundView { + self.gradientBackgroundView = nil + if transition.animation.isImmediate { + gradientBackgroundView.removeFromSuperview() + } else { + gradientBackgroundView.layer.animateAlpha(from: gradientBackgroundView.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak gradientBackgroundView] _ in + gradientBackgroundView?.removeFromSuperview() + }) + } + } + return + } + + let gradientBackgroundView: AnimatedGradientBackgroundView + var gradientTransition = transition + if let current = self.gradientBackgroundView { + gradientBackgroundView = current + } else { + gradientTransition = .immediate + gradientBackgroundView = AnimatedGradientBackgroundView(frame: .zero) + gradientBackgroundView.alpha = 0.0 + self.gradientBackgroundView = gradientBackgroundView + } + + if gradientBackgroundView.superview !== contentContainerView { + gradientBackgroundView.removeFromSuperview() + contentContainerView.insertSubview(gradientBackgroundView, at: 0) + } else { + contentContainerView.sendSubviewToBack(gradientBackgroundView) + } + + gradientBackgroundView.update(size: size, gradient: gradient, cornerRadius: cornerRadius, transition: gradientTransition) + gradientTransition.setFrame(view: gradientBackgroundView, frame: CGRect(origin: .zero, size: size)) + transition.setAlpha(view: gradientBackgroundView, alpha: 1.0) + } + @objc private func pressed() { guard let component = self.component else { return @@ -694,6 +754,8 @@ public final class ButtonComponent: Component { transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius) } + self.updateGradientBackground(component: component, contentContainerView: contentContainerView, size: size, cornerRadius: cornerRadius, transition: transition) + if component.background.style == .glass, component.background.color.alpha > 1.0 - .ulpOfOne { self.updateGlassEffect(component: component, size: size, cornerRadius: cornerRadius, transition: transition) } else { @@ -773,7 +835,11 @@ public final class ButtonComponent: Component { shimmeringTransition = .immediate shimmeringView = ButtonShimmeringView(frame: .zero) self.shimmeringView = shimmeringView - contentContainerView.insertSubview(shimmeringView, at: 0) + if let gradientBackgroundView = self.gradientBackgroundView, gradientBackgroundView.superview === contentContainerView { + contentContainerView.insertSubview(shimmeringView, aboveSubview: gradientBackgroundView) + } else { + contentContainerView.insertSubview(shimmeringView, at: 0) + } } shimmeringView.update(size: size, background: component.background, cornerRadius: cornerRadius, transition: shimmeringTransition) shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: size)) @@ -795,13 +861,15 @@ public final class ButtonComponent: Component { self.chromeView = chromeView if let shimmeringView = self.shimmeringView { contentContainerView.insertSubview(chromeView, aboveSubview: shimmeringView) + } else if let gradientBackgroundView = self.gradientBackgroundView, gradientBackgroundView.superview === contentContainerView { + contentContainerView.insertSubview(chromeView, aboveSubview: gradientBackgroundView) } else { contentContainerView.insertSubview(chromeView, at: 0) } chromeView.layer.compositingFilter = "overlayBlendMode" chromeView.alpha = 0.8 - chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.36, fillColor: .clear) + chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: size.height, height: size.height), isDark: component.background.color.lightness < 0.36, fillColor: .clear) } chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: size)) } else if let chromeView = self.chromeView { @@ -829,6 +897,113 @@ public final class ButtonComponent: Component { } } +private final class AnimatedGradientBackgroundView: UIView { + private let backgroundView: UIImageView + private var animationView: UIImageView? + + private var currentGradient: ButtonComponent.Background.Gradient? + private var currentImageHeight: CGFloat? + + override init(frame: CGRect) { + self.backgroundView = UIImageView() + self.backgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + self.backgroundView.layer.cornerCurve = .continuous + } + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(size: CGSize, gradient: ButtonComponent.Background.Gradient, cornerRadius: CGFloat, transition: ComponentTransition) { + let bounds = CGRect(origin: .zero, size: size) + + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + transition.setCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) + transition.setFrame(view: self.backgroundView, frame: bounds) + + let imageHeight = max(1.0, size.height) + if self.currentGradient != gradient || self.currentImageHeight != imageHeight || self.backgroundView.image == nil { + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(gradient.colors.count - 1) + for i in 0 ..< gradient.colors.count { + locations.append(delta * CGFloat(i)) + } + + let image = generateGradientImage(size: CGSize(width: 200.0, height: imageHeight), colors: gradient.colors, locations: locations, direction: .horizontal) + self.backgroundView.image = image + self.animationView?.image = image + self.animationView?.layer.removeAnimation(forKey: "movement") + + self.currentGradient = gradient + self.currentImageHeight = imageHeight + } + + let animationView: UIImageView + if let current = self.animationView { + animationView = current + } else { + animationView = UIImageView() + animationView.image = self.backgroundView.image + self.animationView = animationView + self.backgroundView.addSubview(animationView) + } + + animationView.bounds = CGRect(origin: .zero, size: CGSize(width: size.width * 2.4, height: size.height)) + if animationView.layer.animation(forKey: "movement") == nil { + animationView.center = CGPoint(x: animationView.bounds.width / 2.0 - animationView.bounds.width * 0.35, y: size.height / 2.0) + } + self.setupGradientAnimations(size: size, gradient: gradient) + } + + private func setupGradientAnimations(size: CGSize, gradient: ButtonComponent.Background.Gradient) { + guard let animationView = self.animationView else { + return + } + + let duration: Double + switch gradient.animation { + case let .horizontalShift(value): + duration = value + } + + if animationView.layer.animation(forKey: "movement") == nil { + let offset = (animationView.bounds.width - size.width) / 2.0 + let previousValue = animationView.center.x + var newValue: CGFloat = offset + if offset - previousValue < animationView.bounds.width * 0.25 { + newValue -= animationView.bounds.width * 0.35 + } + animationView.center = CGPoint(x: newValue, y: animationView.bounds.height / 2.0) + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "position.x") + animation.duration = duration + animation.fromValue = previousValue + animation.toValue = newValue + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations(size: size, gradient: gradient) + } + + animationView.layer.add(animation, forKey: "movement") + CATransaction.commit() + } + } +} + private class ButtonShimmeringView: UIView { private var shimmerView = ShimmerEffectForegroundView() private var borderView = UIView() @@ -872,7 +1047,11 @@ private class ButtonShimmeringView: UIView { compositingFilter = nil } - self.backgroundColor = background.color + if let gradient = background.gradient, gradient.colors.count > 1 { + self.backgroundColor = .clear + } else { + self.backgroundColor = background.color + } self.layer.cornerRadius = cornerRadius self.borderMaskView.layer.cornerRadius = cornerRadius diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 737457540a..9b09ee1d8c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1378,7 +1378,7 @@ private final class CameraScreenComponent: CombinedComponent { let previewHeight = floorToScreenPixels(availableSize.width * 1.77778) if !isTablet { if availableSize.height < previewHeight + 30.0 { - controlsBottomInset = -48.0 + controlsBottomInset = -70.0 } } @@ -1505,6 +1505,7 @@ private final class CameraScreenComponent: CombinedComponent { hasAppeared: component.hasAppeared && hasAllRequiredAccess, hasAccess: hasAllRequiredAccess, hideControls: component.cameraState.collageProgress > 1.0 - .ulpOfOne, + controlsBottomInset: controlsBottomInset, collageProgress: component.cameraState.collageProgress, collageCount: component.cameraState.isCollageEnabled ? component.cameraState.collageGrid.count : nil, tintColor: controlsTintColor, @@ -2111,7 +2112,7 @@ private final class CameraScreenComponent: CombinedComponent { if isTablet { availableModeControlSize = CGSize(width: floor(panelWidth), height: 120.0) } else { - availableModeControlSize = availableSize + availableModeControlSize = CGSize(width: availableSize.width - 140.0, height: availableSize.height) } var availableModes: [CameraState.CameraMode] = [.photo, .video] diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 1fecc3d492..eadde87f2c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -310,7 +310,7 @@ private final class ShutterButtonContentComponent: Component { glassAlpha = 0.0 chromeAlpha = 0.65 labelAlpha = progress ? 0.0 : 1.0 - chromeSize = CGSize(width: 326.0, height: 53.0 - UIScreenPixel) + chromeSize = CGSize(width: 297.0, height: 53.0 - UIScreenPixel) hasProgress = progress } @@ -683,6 +683,7 @@ final class CaptureControlsComponent: Component { let hasAppeared: Bool let hasAccess: Bool let hideControls: Bool + let controlsBottomInset: CGFloat let collageProgress: Float let collageCount: Int? let tintColor: UIColor @@ -712,6 +713,7 @@ final class CaptureControlsComponent: Component { hasAppeared: Bool, hasAccess: Bool, hideControls: Bool, + controlsBottomInset: CGFloat, collageProgress: Float, collageCount: Int?, tintColor: UIColor, @@ -740,6 +742,7 @@ final class CaptureControlsComponent: Component { self.hasAppeared = hasAppeared self.hasAccess = hasAccess self.hideControls = hideControls + self.controlsBottomInset = controlsBottomInset self.collageProgress = collageProgress self.collageCount = collageCount self.tintColor = tintColor @@ -783,6 +786,9 @@ final class CaptureControlsComponent: Component { if lhs.hideControls != rhs.hideControls { return false } + if lhs.controlsBottomInset != rhs.controlsBottomInset { + return false + } if lhs.collageProgress != rhs.collageProgress { return false } @@ -1223,6 +1229,13 @@ final class CaptureControlsComponent: Component { let hideControls = component.hideControls + var bottomButtonSideInset: CGFloat = 16.0 + var bottomButtonTopInset: CGFloat = 21.0 + if component.controlsBottomInset < 0.0 { + bottomButtonSideInset = 9.0 + bottomButtonTopInset = -2.0 - UIScreenPixel + } + let galleryButtonFrame: CGRect let lockReferenceFrame: CGRect let gallerySize: CGSize @@ -1268,7 +1281,7 @@ final class CaptureControlsComponent: Component { galleryButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - galleryButtonSize.width) / 2.0), y: size.height - galleryButtonSize.height - 56.0), size: galleryButtonSize) lockReferenceFrame = .zero } else { - galleryButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: size.height + 21.0), size: galleryButtonSize) + galleryButtonFrame = CGRect(origin: CGPoint(x: bottomButtonSideInset, y: size.height + bottomButtonTopInset), size: galleryButtonSize) lockReferenceFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: floorToScreenPixels((size.height - galleryButtonSize.height) / 2.0)), size: galleryButtonSize) } if let galleryButtonView = self.galleryButtonView.view as? CameraButton.View { @@ -1360,7 +1373,7 @@ final class CaptureControlsComponent: Component { environment: {}, containerSize: availableSize ) - let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - bottomFlipButtonSize.width - 16.0, y: 21.0), size: bottomFlipButtonSize) + let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - bottomFlipButtonSize.width - bottomButtonSideInset, y: bottomButtonTopInset), size: bottomFlipButtonSize) if let bottomFlipButtonView = self.bottomFlipButton.view { if bottomFlipButtonView.superview == nil { self.bottomContainerView.contentView.addSubview(bottomFlipButtonView) @@ -1399,13 +1412,13 @@ final class CaptureControlsComponent: Component { environment: {}, containerSize: availableSize ) - let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 21.0), size: bottomSettingsButtonSize) + let bottomSettingsButtonFrame = CGRect(origin: CGPoint(x: bottomButtonSideInset, y: bottomButtonTopInset), size: bottomSettingsButtonSize) if let bottomSettingsButtonView = self.bottomSettingsButton.view { if bottomSettingsButtonView.superview == nil { self.bottomContainerView.contentView.addSubview(bottomSettingsButtonView) } - transition.setBounds(view: bottomSettingsButtonView, bounds: CGRect(origin: .zero, size: bottomFlipButtonFrame.size)) - transition.setPosition(view: bottomSettingsButtonView, position: bottomFlipButtonFrame.center) + transition.setBounds(view: bottomSettingsButtonView, bounds: CGRect(origin: .zero, size: bottomSettingsButtonFrame.size)) + transition.setPosition(view: bottomSettingsButtonView, position: bottomSettingsButtonFrame.center) transition.setScale(view: bottomSettingsButtonView, scale: !isLiveStream || isLiveActive || isRecording || isTransitioning || hideControls ? 0.01 : 1.0) transition.setAlpha(view: bottomSettingsButtonView, alpha: !isLiveStream || isLiveActive || isRecording || isTransitioning || hideControls ? 0.0 : 1.0) @@ -1461,7 +1474,7 @@ final class CaptureControlsComponent: Component { let shutterButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - shutterButtonSize.width) / 2.0, y: (size.height - shutterButtonSize.height) / 2.0), size: shutterButtonSize) let guideSpacing: CGFloat = 9.0 - let guideSize = CGSize(width: isHolding ? component.isTablet ? 84.0 : 60.0 : 0.0, height: 1.0 + UIScreenPixel) + let guideSize = CGSize(width: isHolding ? (component.isTablet ? 84.0 : 60.0) : 0.0, height: 1.0 + UIScreenPixel) let guideAlpha: CGFloat = isHolding ? 1.0 : 0.0 let leftGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.minX - guideSpacing - guideSize.width, y: floorToScreenPixels((size.height - guideSize.height) / 2.0)), size: guideSize) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index fc56774d12..2bfcd3b081 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -70,7 +70,20 @@ final class ModeComponent: Component { return true } - final class View: UIView, ComponentTaggedView { + final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct LayoutData { + var containerSize: CGSize + var selectedFrame: CGRect + var cornerRadius: CGFloat? + var isTablet: Bool + } + private var component: ModeComponent? private var state: EmptyComponentState? @@ -107,6 +120,10 @@ final class ModeComponent: Component { private var backgroundContainer = GlassBackgroundContainerView() private var liquidLensView: LiquidLensView? + private let scrollView = ScrollView() + private let selectedScrollView = UIView() + private var ignoreScrolling = false + private var layoutData: LayoutData? private var itemViews: [AnyHashable: ItemView] = [:] private var selectedItemViews: [AnyHashable: ItemView] = [:] @@ -132,6 +149,27 @@ final class ModeComponent: Component { self.layer.allowsGroupOpacity = true + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = true + self.scrollView.delegate = self + self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + return self.scrollView.contentOffset.x > .ulpOfOne + } + + self.selectedScrollView.clipsToBounds = true + self.selectedScrollView.isUserInteractionEnabled = false + self.addSubview(self.backgroundView) self.backgroundView.addSubview(self.backgroundContainer) } @@ -159,13 +197,14 @@ final class ModeComponent: Component { return self.backgroundView.frame.contains(point) } - private func item(at point: CGPoint) -> AnyHashable? { + private func item(at point: CGPoint, in view: UIView) -> AnyHashable? { var closestItem: (AnyHashable, CGFloat)? for (id, itemView) in self.itemViews { - if itemView.frame.contains(point) { + let itemFrame = itemView.convert(itemView.bounds, to: view) + if itemFrame.contains(point) { return id } else { - let distance = abs(point.x - itemView.center.x) + let distance = abs(point.x - itemFrame.midX) if let closestItemValue = closestItem { if closestItemValue.1 > distance { closestItem = (id, distance) @@ -178,22 +217,46 @@ final class ModeComponent: Component { return closestItem?.0 } - @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { - guard let component = self.component, let liquidLensView = self.liquidLensView else { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { return } - let location = recognizer.location(in: liquidLensView.contentView) + self.updateScrolling(transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + return false + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self) switch recognizer.state { case .began: - if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] { let startX = itemView.frame.minX - 4.0 self.selectionGestureState = (startX, startX, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: if var selectionGestureState = self.selectionGestureState { + let translation = recognizer.translation(in: self) + if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) { + self.selectionGestureState = nil + recognizer.state = .cancelled + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + return + } selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x - if let itemId = self.item(at: location) { + if let itemId = self.item(at: location, in: self) { selectionGestureState.itemId = itemId } self.selectionGestureState = selectionGestureState @@ -214,8 +277,38 @@ final class ModeComponent: Component { break } } + + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else { + return + } + + let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX + var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size) + if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet { + lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0) + } + + if layoutData.isTablet { + lensSelection.size.width = layoutData.containerSize.width + } else { + lensSelection.origin.x -= contentOffsetX + lensSelection.origin.y = 0.0 + lensSelection.size.height = layoutData.containerSize.height + } + + let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width) + transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize)) + transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize)) + + liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate) + + self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne + } func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component self.component = component self.state = state @@ -228,16 +321,36 @@ final class ModeComponent: Component { liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer) self.liquidLensView = liquidLensView self.backgroundContainer.contentView.addSubview(liquidLensView) + liquidLensView.contentView.addSubview(self.scrollView) + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + tabSelectionRecognizer.delegate = self + tabSelectionRecognizer.cancelsTouchesInView = false self.tabSelectionRecognizer = tabSelectionRecognizer liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } + if self.scrollView.superview == nil { + liquidLensView.contentView.addSubview(self.scrollView) + } + if self.selectedScrollView.superview == nil { + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + } self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) - let inset: CGFloat = 23.0 - let spacing: CGFloat = isTablet ? 9.0 : 40.0 + var inset: CGFloat = 23.0 + let spacing: CGFloat + if isTablet { + spacing = 9.0 + } else { + if availableSize.width < 200.0 { + inset = 20.0 + spacing = 24.0 + } else { + spacing = 40.0 + } + } var i = 0 var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize) @@ -257,12 +370,16 @@ final class ModeComponent: Component { itemView = ItemView() itemView.isUserInteractionEnabled = false self.itemViews[id] = itemView - liquidLensView.contentView.addSubview(itemView) selectedItemView = ItemView() selectedItemView.isUserInteractionEnabled = false self.selectedItemViews[id] = selectedItemView - liquidLensView.selectedContentView.addSubview(selectedItemView) + } + if itemView.superview !== self.scrollView { + self.scrollView.addSubview(itemView) + } + if selectedItemView.superview !== self.selectedScrollView { + self.selectedScrollView.addSubview(selectedItemView) } let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) @@ -297,47 +414,68 @@ final class ModeComponent: Component { transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) + + if let selectedItemView = self.selectedItemViews[id] { + transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in + selectedItemView.removeFromSuperview() + }) + } } } for id in removeKeys { self.itemViews.removeValue(forKey: id) + self.selectedItemViews.removeValue(forKey: id) } let totalSize: CGSize let size: CGSize + let contentSize: CGSize var cornerRadius: CGFloat? if isTablet { totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) size = CGSize(width: availableSize.width, height: availableSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize)) + contentSize = totalSize cornerRadius = 20.0 } else { size = CGSize(width: availableSize.width, height: buttonSize.height) totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalSize.width) / 2.0), y: 0.0), size: totalSize)) + let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize)) + contentSize = totalSize } let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) transition.setFrame(view: self.backgroundContainer, frame: containerFrame) - - let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0) - var lensSelection: (origin: CGPoint, size: CGSize) - if let selectionGestureState = self.selectionGestureState, !isTablet { - lensSelection = (CGPoint(x: selectionGestureState.currentX, y: 0.0), selectionFrame.size) - } else { - lensSelection = (CGPoint(x: selectionFrame.minX, y: selectionFrame.minY), selectionFrame.size) - } - - if isTablet { - lensSelection.size.width = size.width - } else { - lensSelection.size.height = containerFrame.size.height - lensSelection.origin.y = 0.0 - } - transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) - liquidLensView.update(size: containerFrame.size, cornerRadius: cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, containerFrame.size.width - lensSelection.size.width)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !isTablet, isCollapsed: false, transition: transition) - self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate) + + let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size) + transition.setFrame(view: self.scrollView, frame: scrollViewFrame) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne + + self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet) + + self.ignoreScrolling = true + var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size) + let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width) + let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size + if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem { + let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25) + if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX { + scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead + } + if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead { + scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead + } + } + scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX)) + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) return size } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index ed799fd9db..72289e0be8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -43,7 +43,7 @@ final class ShutterBlobView: UIView { case .generic, .video, .transientToFlip: return CGSize(width: 0.63, height: 0.63) case .live: - return CGSize(width: 3.4, height: 0.55) + return CGSize(width: 3.1, height: 0.55) case .transientToLock, .lock, .stopVideo: return CGSize(width: 0.275, height: 0.275) } diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index f9b1e28894..9acaf362e6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -247,6 +247,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg self.scrollNode.view.clipsToBounds = true self.scrollNode.cornerRadius = 30.0 self.scrollNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.scrollNode.view.scrollsToTop = false self.backgroundTintMaskView.clipsToBounds = true self.backgroundTintMaskView.layer.cornerRadius = 30.0 @@ -380,14 +381,14 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg var dismissIfOnce = false switch markupButton.action { case .text: - self.controllerInteraction.sendMessage(markupButton.title) + self.controllerInteraction.sendMessage(markupButton.title, self.message?.id) dismissIfOnce = true case let .url(url): self.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: true, progress: Promise())) case .requestMap: - self.controllerInteraction.shareCurrentLocation() + self.controllerInteraction.shareCurrentLocation(self.message?.id) case .requestPhone: - self.controllerInteraction.shareAccountContact() + self.controllerInteraction.shareAccountContact(self.message?.id) case .openWebApp: if let message = self.message { self.controllerInteraction.requestMessageActionCallback(message._asMessage(), nil, true, false, nil) @@ -428,7 +429,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg self.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) } case let .setupPoll(isQuiz): - self.controllerInteraction.openPollCreation(isQuiz) + self.controllerInteraction.openPollCreation(self.message?.id, isQuiz) case let .openUserProfile(peerId): let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 93b4efb11f..6313e641bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -1114,6 +1114,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, super.init(frame: CGRect(), textContainer: self.displayInternal.textContainer, disableTiling: disableTiling) self.delegate = self + self.scrollsToTop = false if #available(iOS 18.0, *) { self.supportsAdaptiveImageGlyph = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index b058fc2230..34e85d7e37 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -796,7 +796,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var currentInputParams: Params? private var currentApplyParams: ListViewItemApply? private var contentLayoutInsets = UIEdgeInsets() - + + private func isQuickReplyMessageInputCustomContents() -> Bool { + guard let item = self.item else { + return false + } + if case let .customChatContents(contents) = item.associatedData.subject, case .quickReplyMessageInput = contents.kind { + return true + } + return false + } + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -832,12 +842,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let item = strongSelf.item, item.controllerInteraction.focusedPollAddOptionMessageId != nil { return .none } - if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { - if case let .action(action) = action, !action.contextMenuOnLongPress { - return .none - } - } - if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + + let resolveLongTapAction: (InternalBubbleTapAction) -> ContextControllerSourceNode.ShouldBegin = { action in switch action { case .action: return .none @@ -855,6 +861,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } + + let isQuickReplyMessageInputCustomContents = strongSelf.isQuickReplyMessageInputCustomContents() + if isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } + if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { + if case let .action(action) = action, !action.contextMenuOnLongPress { + return .none + } + } + if !isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } return .default } @@ -4636,12 +4655,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if strongSelf.selectionNode != nil { return false } - if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { - if case .action = action { - return false - } - } - if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + + let resolveLongTapAction: (InternalBubbleTapAction) -> Bool = { action in switch action { case .action, .optionalAction: return false @@ -4649,6 +4664,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return !openContextMenu.selectAll } } + + let isQuickReplyMessageInputCustomContents = strongSelf.isQuickReplyMessageInputCustomContents() + if isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } + if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { + if case .action = action { + return false + } + } + if !isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } return true } containerNode.activated = { [weak strongSelf, weak containerNode] gesture, location in @@ -5420,7 +5448,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } - if item.message.timestamp < 10 { + if item.message.timestamp < 10 && !strongSelf.isQuickReplyMessageInputCustomContents() { hasMenuGesture = false } strongSelf.mainContainerNode.isGestureEnabled = hasMenuGesture diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index da8399997d..8499f95c2d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -46,6 +46,19 @@ private func mediaMergeableStyle(_ media: EngineRawMedia) -> ChatMessageMerge { return .fullyMerged } +private func anonymousGroupAdminSignature(message: EngineRawMessage, effectiveAuthor: EngineRawPeer?) -> String? { + guard let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info else { + return nil + } + guard effectiveAuthor?.id == channel.id else { + return nil + } + guard let signature = message.authorSignatureAttribute?.signature, !signature.isEmpty else { + return nil + } + return signature +} + private func messagesShouldBeMerged(accountPeerId: EnginePeer.Id, _ lhs: EngineRawMessage, _ rhs: EngineRawMessage) -> ChatMessageMerge { var lhsEffectiveAuthor: EngineRawPeer? = lhs.author var rhsEffectiveAuthor: EngineRawPeer? = rhs.author @@ -111,6 +124,14 @@ private func messagesShouldBeMerged(accountPeerId: EnginePeer.Id, _ lhs: EngineR sameAuthor = false } + if sameAuthor { + let lhsAnonymousAdminSignature = anonymousGroupAdminSignature(message: lhs, effectiveAuthor: lhsEffectiveAuthor) + let rhsAnonymousAdminSignature = anonymousGroupAdminSignature(message: rhs, effectiveAuthor: rhsEffectiveAuthor) + if lhsAnonymousAdminSignature != rhsAnonymousAdminSignature && (lhsAnonymousAdminSignature != nil || rhsAnonymousAdminSignature != nil) { + sameAuthor = false + } + } + var lhsEffectiveTimestamp = lhs.timestamp var rhsEffectiveTimestamp = rhs.timestamp diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 8850c8f1c3..684c085925 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -811,7 +811,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { if let item = self.item { switch button.action { case .text: - item.controllerInteraction.sendMessage(button.title) + item.controllerInteraction.sendMessage(button.title, item.message.id) case let .url(url): var concealed = true if url.hasPrefix("tg://") { @@ -819,9 +819,9 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { } item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, progress: progress)) case .requestMap: - item.controllerInteraction.shareCurrentLocation() + item.controllerInteraction.shareCurrentLocation(item.message.id) case .requestPhone: - item.controllerInteraction.shareAccountContact() + item.controllerInteraction.shareAccountContact(item.message.id) case .openWebApp: item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, progress) case let .callback(requiresPassword, data): diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 078cd9961f..795171b893 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2940,17 +2940,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var isRestricted = false if let poll = poll { - if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) { - isRestricted = true - } - if poll.restrictToSubscribers { - let period: Int32 = item.context.account.testingEnvironment ? 5 * 60 : 24 * 60 * 60 - if !item.associatedData.isParticipant { - isRestricted = true - } else if let invitedOn = item.associatedData.invitedOn, invitedOn + period > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { - isRestricted = true - } - } + isRestricted = item.associatedData.isPollVotingRestricted( + poll: poll, + accountTestingEnvironment: item.context.account.testingEnvironment, + currentTimestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + ) orderedPollOptions = resolvedOptionOrder(for: item) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index b76dd20221..adb73ad6fd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -135,21 +135,6 @@ private final class GlassButtonView: UIView { super.init(frame: frame) self.addSubview(self.backgroundView) - - if #available(iOS 26.0, *) { - } else { - self.button.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted && self.isEnabled && !self.isImplicitlyDisabled { - self.backgroundView.contentView.alpha = 0.6 - } else { - self.backgroundView.contentView.alpha = 1.0 - self.backgroundView.contentView.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - } } required init?(coder: NSCoder) { @@ -176,7 +161,8 @@ private final class GlassButtonView: UIView { transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: params.size)) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) - self.backgroundView.update(size: params.size, cornerRadius: min(params.size.width, params.size.height) * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: isEnabled, transition: transition) + self.backgroundView.update(size: params.size, cornerRadius: min(params.size.width, params.size.height) * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition) + self.backgroundView.isUserInteractionEnabled = isEnabled self.iconView.alpha = isEnabled ? 1.0 : 0.5 self.iconView.tintMask.alpha = self.iconView.alpha diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index d250fb133b..b32cf1027d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -827,6 +827,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.scrollNodeContentNode = ASDisplayNode() self.scrollNodeContentNode.clipsToBounds = true diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 82934145c6..bdf4739e9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -313,7 +313,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let context = self?.context, let navigationController = self?.getNavigationController() { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always, animated: true).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, editGif: { _, _ in }, requestMessageActionCallback: { [weak self] message, _, _, _, _ in guard let self else { @@ -327,7 +327,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in self?.openUrl(url.url, progress: url.progress) }, openExternalInstantPage: { _ in - }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in + }, shareCurrentLocation: { _ in }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { navigationController.pushViewController(controller) @@ -611,7 +611,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index 7c68086f5c..a1e5b6d07c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -152,6 +152,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 85ac8110b9..c38f82c1c6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -423,11 +423,11 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { _ in }, clickThroughMessage: { _, _ in - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, editGif: { _, _ in }, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in - }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in + }, shareCurrentLocation: { _ in }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -459,7 +459,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift index 1aca3bf9a1..61e5a4d228 100644 --- a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift @@ -229,6 +229,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.scrollNode = ASScrollNode() self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.clipsToBounds = true + self.scrollNode.view.scrollsToTop = false self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -236,6 +237,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.list.itemPlaceholderTextColor) self.textFieldScrollNode = ASScrollNode() + self.textFieldScrollNode.view.scrollsToTop = false self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 1387502783..d7b308ff93 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -209,7 +209,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let clickThroughMessage: (UIView?, CGPoint?) -> Void public let toggleMessagesSelection: ([EngineMessage.Id], Bool) -> Void public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void - public let sendMessage: (String) -> Void + public let sendMessage: (String, EngineMessage.Id?) -> Void public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [EngineItemCollectionId]) -> Bool public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool @@ -220,8 +220,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let activateSwitchInline: (EnginePeer.Id?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void public let openUrl: (OpenUrl) -> Void public let openExternalInstantPage: (OpenInstantPage) -> Void - public let shareCurrentLocation: () -> Void - public let shareAccountContact: () -> Void + public let shareCurrentLocation: (EngineMessage.Id?) -> Void + public let shareAccountContact: (EngineMessage.Id?) -> Void public let sendBotCommand: (EngineMessage.Id?, String) -> Void public let openInstantPage: (EngineRawMessage, ChatMessageItemAssociatedData?) -> Void public let openWallpaper: (EngineRawMessage) -> Void @@ -264,7 +264,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let displaySwipeToReplyHint: () -> Void public let dismissReplyMarkupMessage: (EngineRawMessage) -> Void public let openMessagePollResults: (EngineMessage.Id, Data) -> Void - public let openPollCreation: (Bool?) -> Void + public let openPollCreation: (EngineMessage.Id?, Bool?) -> Void public let openPollMedia: (EngineRawMessage, PollMediaSubject) -> Void public let displayPollSolution: (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void public let displayPsa: (String, ASDisplayNode) -> Void @@ -389,7 +389,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void, toggleMessagesSelection: @escaping ([EngineMessage.Id], Bool) -> Void, sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void, - sendMessage: @escaping (String) -> Void, + sendMessage: @escaping (String, EngineMessage.Id?) -> Void, sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [EngineItemCollectionId]) -> Bool, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, @@ -400,8 +400,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol activateSwitchInline: @escaping (EnginePeer.Id?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void, openUrl: @escaping (OpenUrl) -> Void, openExternalInstantPage: @escaping (OpenInstantPage) -> Void, - shareCurrentLocation: @escaping () -> Void, - shareAccountContact: @escaping () -> Void, + shareCurrentLocation: @escaping (EngineMessage.Id?) -> Void, + shareAccountContact: @escaping (EngineMessage.Id?) -> Void, sendBotCommand: @escaping (EngineMessage.Id?, String) -> Void, openInstantPage: @escaping (EngineRawMessage, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (EngineRawMessage) -> Void, @@ -444,7 +444,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (EngineRawMessage) -> Void, openMessagePollResults: @escaping (EngineMessage.Id, Data) -> Void, - openPollCreation: @escaping (Bool?) -> Void, + openPollCreation: @escaping (EngineMessage.Id?, Bool?) -> Void, openPollMedia: @escaping (EngineRawMessage, PollMediaSubject) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 46454d83ca..aad31d517d 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -609,7 +609,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let query = text.trimmingCharacters(in: .whitespacesAndNewlines) let signal: Signal<(StickerPaneSearchStickerState, FoundStickerSets, Bool, FoundStickerSets?)?, NoError> - if query.count >= 2 { + if query.isSingleEmoji || query.count >= 2 { let context = self.context let stickers: Signal if query.isSingleEmoji { diff --git a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift index d23452ebc4..859cc937b3 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift @@ -559,6 +559,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false self.backgroundView.contentView.addSubview(self.scrollNode.view) self.scrollNode.addSubnode(self.selectedBackgroundNode) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift index 42df461552..e0d642a4d7 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift @@ -381,9 +381,9 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { let _ = titleLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true)) if case .center = alignment { - strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset + 1.0), size: titleLayout.0.size) } else { - strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 1.0), size: titleLayout.0.size) } let _ = textLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true)) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift index 4048b8da84..aff49df2a4 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift @@ -55,6 +55,7 @@ public final class ChatListSearchFiltersContainerNode: ASDisplayNode { self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.contentInsetAdjustmentBehavior = .never + self.scrollNode.view.scrollsToTop = false self.view.addSubview(self.backgroundContainer) diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift index e4be5f979c..9c9c5234a3 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift @@ -61,6 +61,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD index fd44f1f568..e056bd9096 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD @@ -13,6 +13,11 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/ViewControllerComponent", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/ComponentFlow", @@ -20,13 +25,11 @@ swift_library( "//submodules/TelegramUIPreferences", "//submodules/PresentationDataUtils", "//submodules/TelegramNotices", - "//submodules/AnimationUI", "//submodules/MergeLists", "//submodules/MediaResources", "//submodules/StickerResources", "//submodules/WallpaperResources", "//submodules/TooltipUI", - "//submodules/SolidRoundedButtonNode", "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", "//submodules/ShimmerEffect", @@ -35,6 +38,9 @@ swift_library( "//submodules/Markdown", "//submodules/AppBundle", "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index ed51ead95d..6bbd3dad85 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -2,15 +2,16 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import ComponentFlow +import ComponentDisplayAdapters +import ViewControllerComponent import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import TelegramUIPreferences import TelegramNotices import PresentationDataUtils -import AnimationUI import MergeLists import MediaResources import StickerResources @@ -22,6 +23,12 @@ import ShimmerEffect import AttachmentUI import AvatarNode import AlertComponent +import SheetComponent +import ButtonComponent +import GlassBarButtonComponent +import BundleIconComponent +import LottieComponent +import MultilineTextComponent private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int @@ -440,7 +447,11 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } if item.themeReference == nil { - strongSelf.imageNode.backgroundColor = item.theme.list.plainBackgroundColor + if item.theme.overallDarkAppearance { + strongSelf.imageNode.backgroundColor = item.theme.list.plainBackgroundColor + } else { + strongSelf.imageNode.backgroundColor = item.theme.rootController.navigationBar.segmentedForegroundColor + } } if updatedTheme || updatedSelected { @@ -619,8 +630,8 @@ public final class ChatThemeScreen: ViewController { public static let themeCrossfadeDuration: Double = 0.3 public static let themeCrossfadeDelay: Double = 0.25 - private var controllerNode: ChatThemeScreenNode { - return self.displayNode as! ChatThemeScreenNode + private var controllerNode: ChatThemeSheetScreenNode { + return self.displayNode as! ChatThemeSheetScreenNode } private var animatedIn = false @@ -699,7 +710,7 @@ public final class ChatThemeScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedTheme: self.initiallySelectedTheme, peerName: self.peerName) + self.displayNode = ChatThemeSheetScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedTheme: self.initiallySelectedTheme, peerName: self.peerName) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.previewTheme = { [weak self] chatTheme, dark in guard let strongSelf = self else { @@ -805,214 +816,418 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f return colors } -private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private weak var controller: ChatThemeScreen? +private final class ChatThemeScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let topContentContainerNode: SparseNode - private let buttonsContentContainerNode: SparseNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButtonNode: WebAppCancelButtonNode - private let switchThemeButton: HighlightTrackingButtonNode - private let animationContainerNode: ASDisplayNode - private var animationNode: AnimationNode - private let doneButton: SolidRoundedButtonNode - private let otherButton: HighlightableButtonNode + let context: AccountContext + let presentationData: PresentationData + let animatedEmojiStickers: [String: [StickerPackItem]] + let initiallySelectedTheme: ChatTheme? + let peerName: String + let canResetWallpaper: Bool + let present: (ViewController) -> Void + let presentInRoot: (ViewController) -> Void + let previewTheme: (ChatTheme?, Bool?) -> Void + let changeWallpaper: () -> Void + let resetWallpaper: () -> Void + let completion: (ChatTheme?) -> Void + let cancel: () -> Void - private let listNode: ListView - private var entries: [ThemeSettingsThemeEntry]? - private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] - private var initialized = false - - private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext - private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? - - private let peerName: String - - private let initiallySelectedTheme: ChatTheme? - private var selectedTheme: ChatTheme? { - didSet { - self.selectedThemePromise.set(self.selectedTheme) - } - } - private var selectedThemePromise: ValuePromise - - private var isDarkAppearancePromise: ValuePromise - private var isDarkAppearance: Bool = false { - didSet { - self.isDarkAppearancePromise.set(self.isDarkAppearance) - } - } - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - private let disposable = MetaDisposable() - - var present: ((ViewController) -> Void)? - var previewTheme: ((ChatTheme?, Bool?) -> Void)? - var completion: ((ChatTheme?) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedTheme: ChatTheme?, peerName: String) { + init( + context: AccountContext, + presentationData: PresentationData, + animatedEmojiStickers: [String: [StickerPackItem]], + initiallySelectedTheme: ChatTheme?, + peerName: String, + canResetWallpaper: Bool, + present: @escaping (ViewController) -> Void, + presentInRoot: @escaping (ViewController) -> Void, + previewTheme: @escaping (ChatTheme?, Bool?) -> Void, + changeWallpaper: @escaping () -> Void, + resetWallpaper: @escaping () -> Void, + completion: @escaping (ChatTheme?) -> Void, + cancel: @escaping () -> Void + ) { self.context = context - self.controller = controller + self.presentationData = presentationData + self.animatedEmojiStickers = animatedEmojiStickers self.initiallySelectedTheme = initiallySelectedTheme self.peerName = peerName - self.selectedTheme = initiallySelectedTheme - self.selectedThemePromise = ValuePromise(initiallySelectedTheme) - self.presentationData = presentationData + self.canResetWallpaper = canResetWallpaper + self.present = present + self.presentInRoot = presentInRoot + self.previewTheme = previewTheme + self.changeWallpaper = changeWallpaper + self.resetWallpaper = resetWallpaper + self.completion = completion + self.cancel = cancel + } + + static func ==(lhs: ChatThemeScreenComponent, rhs: ChatThemeScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.presentationData.theme !== rhs.presentationData.theme { + return false + } + if lhs.presentationData.strings !== rhs.presentationData.strings { + return false + } + if lhs.initiallySelectedTheme != rhs.initiallySelectedTheme { + return false + } + if lhs.peerName != rhs.peerName { + return false + } + if lhs.canResetWallpaper != rhs.canResetWallpaper { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() - self.uniqueGiftChatThemesContext = UniqueGiftChatThemesContext(account: context.account) + private var component: ChatThemeScreenComponent? + private var environment: ViewControllerComponentContainer.Environment? - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true + override init(frame: CGRect) { + super.init(frame: frame) + } - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = .clear + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false + func update(component: ChatThemeScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environmentValue + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: false, + hasInputHeight: !environmentValue.inputHeight.isZero, + regularMetricsSize: nil, + dismiss: { _ in + component.cancel() + } + ) + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(ChatThemeSheetContentComponent( + context: component.context, + presentationData: component.presentationData, + animatedEmojiStickers: component.animatedEmojiStickers, + initiallySelectedTheme: component.initiallySelectedTheme, + peerName: component.peerName, + canResetWallpaper: component.canResetWallpaper, + present: component.present, + presentInRoot: component.presentInRoot, + previewTheme: component.previewTheme, + changeWallpaper: component.changeWallpaper, + resetWallpaper: component.resetWallpaper, + completion: component.completion, + cancel: component.cancel + )), + style: .glass, + backgroundColor: .color(environmentValue.theme.actionSheet.opaqueItemBackgroundColor), + clipsContent: true, + isScrollEnabled: false, + hasDimView: false, + animateOut: self.sheetAnimateOut + )), + environment: { + environmentValue + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: sheetSize)) + } + + return availableSize + } - self.topContentContainerNode = SparseNode() - self.topContentContainerNode.isOpaque = false + private func contentView() -> ChatThemeSheetContentComponent.View? { + guard let sheetView = self.sheet.view else { + return nil + } + return findTaggedComponentViewImpl(view: sheetView, tag: ChatThemeSheetContentComponent.Tag()) as? ChatThemeSheetContentComponent.View + } + + func containsContent(point: CGPoint) -> Bool { + guard let contentView = self.contentView() else { + return false + } + return contentView.bounds.contains(self.convert(point, to: contentView)) + } + + func dimTapped() { + self.contentView()?.dimTapped() + } + + func animateOut(completion: @escaping () -> Void) { + self.contentView()?.setAnimatedOut() + self.sheetAnimateOut.invoke(Action { _ in + completion() + }) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} - self.buttonsContentContainerNode = SparseNode() - self.buttonsContentContainerNode.isOpaque = false +private final class ChatThemeSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + final class Tag { + } + + let context: AccountContext + let presentationData: PresentationData + let animatedEmojiStickers: [String: [StickerPackItem]] + let initiallySelectedTheme: ChatTheme? + let peerName: String + let canResetWallpaper: Bool + let present: (ViewController) -> Void + let presentInRoot: (ViewController) -> Void + let previewTheme: (ChatTheme?, Bool?) -> Void + let changeWallpaper: () -> Void + let resetWallpaper: () -> Void + let completion: (ChatTheme?) -> Void + let cancel: () -> Void + + init( + context: AccountContext, + presentationData: PresentationData, + animatedEmojiStickers: [String: [StickerPackItem]], + initiallySelectedTheme: ChatTheme?, + peerName: String, + canResetWallpaper: Bool, + present: @escaping (ViewController) -> Void, + presentInRoot: @escaping (ViewController) -> Void, + previewTheme: @escaping (ChatTheme?, Bool?) -> Void, + changeWallpaper: @escaping () -> Void, + resetWallpaper: @escaping () -> Void, + completion: @escaping (ChatTheme?) -> Void, + cancel: @escaping () -> Void + ) { + self.context = context + self.presentationData = presentationData + self.animatedEmojiStickers = animatedEmojiStickers + self.initiallySelectedTheme = initiallySelectedTheme + self.peerName = peerName + self.canResetWallpaper = canResetWallpaper + self.present = present + self.presentInRoot = presentInRoot + self.previewTheme = previewTheme + self.changeWallpaper = changeWallpaper + self.resetWallpaper = resetWallpaper + self.completion = completion + self.cancel = cancel + } + + static func ==(lhs: ChatThemeSheetContentComponent, rhs: ChatThemeSheetContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.presentationData.theme !== rhs.presentationData.theme { + return false + } + if lhs.presentationData.strings !== rhs.presentationData.strings { + return false + } + if lhs.initiallySelectedTheme != rhs.initiallySelectedTheme { + return false + } + if lhs.peerName != rhs.peerName { + return false + } + if lhs.canResetWallpaper != rhs.canResetWallpaper { + return false + } + return true + } + + final class View: UIView, ComponentTaggedView { + private enum PrimaryAction: Equatable { + case chooseWallpaper + case resetTheme + case apply + } - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 + func matches(tag: Any) -> Bool { + return tag is Tag + } - self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance - self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) + private let title = ComponentView() + private let subtitle = ComponentView() + private let leftButton = ComponentView() + private let switchThemeButton = ComponentView() + private let primaryButton = ComponentView() + private var resetWallpaperButton: ComponentView? + private let switchThemePlayOnce = ActionSlot() - let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let secondaryTextColor = self.presentationData.theme.actionSheet.secondaryTextColor - let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) + private var component: ChatThemeSheetContentComponent? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor + private var selectedTheme: ChatTheme? + private var isDarkAppearance: Bool = false + private var isSwitchThemeEnabled = true + private var isCompleting = false - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Title, font: Font.semibold(17.0), textColor: textColor) + private var themes: [TelegramTheme] = [] + private var uniqueGiftChatThemesContext: UniqueGiftChatThemesContext? + private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? + private var uniqueGiftPeers: [EnginePeer.Id: EnginePeer] = [:] + private let disposable = MetaDisposable() - self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Subtitle(peerName).string, font: Font.regular(15.0), textColor: secondaryTextColor) - self.textNode.isHidden = true + private var themeSelectionsCount = 0 + private var displayedPreviewTooltip = false + private var animatedOut = false - self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - - self.switchThemeButton = HighlightTrackingButtonNode() - self.animationContainerNode = ASDisplayNode() - self.animationContainerNode.isUserInteractionEnabled = false - - self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) - self.animationNode.isUserInteractionEnabled = false - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), glass: true, height: 52.0, cornerRadius: 26.0) - - self.otherButton = HighlightableButtonNode() - - self.listNode = ListViewImpl() - self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.updateButtons() - - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.wrappingScrollNode.addSubnode(self.topContentContainerNode) - self.wrappingScrollNode.addSubnode(self.buttonsContentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.buttonsContentContainerNode.addSubnode(self.textNode) - self.buttonsContentContainerNode.addSubnode(self.doneButton) - self.buttonsContentContainerNode.addSubnode(self.otherButton) - - self.topContentContainerNode.addSubnode(self.animationContainerNode) - self.animationContainerNode.addSubnode(self.animationNode) - self.topContentContainerNode.addSubnode(self.switchThemeButton) - self.topContentContainerNode.addSubnode(self.listNode) - self.topContentContainerNode.addSubnode(self.cancelButtonNode) - - self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) - self.cancelButtonNode.buttonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self { - if strongSelf.doneButton.font == .bold { - strongSelf.complete() - } else { - strongSelf.controller?.changeWallpaper() + override init(frame: CGRect) { + self.listNode = ListViewImpl() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(frame: frame) + + self.addSubview(self.listNode.view) + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else { + return + } + if case let .known(value) = offset, value < 100.0 { + self.uniqueGiftChatThemesContext?.loadMore() } } } - self.otherButton.addTarget(self, action: #selector(self.otherButtonPressed), forControlEvents: .touchUpInside) - self.disposable.set(combineLatest( - queue: Queue.mainQueue(), - self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), - self.uniqueGiftChatThemesContext.state - |> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in - var peerIds: [EnginePeer.Id] = [] - for theme in state.themes { - if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { - peerIds.append(themePeerId) - } - } - return combineLatest( - .single(state), - context.engine.data.get( - EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) - ) |> map { peers in - var result: [EnginePeer.Id: EnginePeer] = [:] - for peerId in peerIds { - if let maybePeer = peers[peerId], let peer = maybePeer { - result[peerId] = peer - } - } - return result - } - ) - }, - self.selectedThemePromise.get(), - self.isDarkAppearancePromise.get() - ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in - guard let strongSelf = self else { + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + + func setAnimatedOut() { + self.animatedOut = true + } + + private func setupIfNeeded(component: ChatThemeSheetContentComponent) { + guard self.uniqueGiftChatThemesContext == nil else { return } - let (uniqueGiftChatThemesState, peers) = uniqueGiftChatThemesStateAndPeers - strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState - - let isFirstTime = strongSelf.entries == nil - let presentationData = strongSelf.presentationData - + + self.selectedTheme = component.initiallySelectedTheme + self.isDarkAppearance = component.presentationData.theme.overallDarkAppearance + + let uniqueGiftChatThemesContext = UniqueGiftChatThemesContext(account: component.context.account) + self.uniqueGiftChatThemesContext = uniqueGiftChatThemesContext + + let context = component.context + self.disposable.set(combineLatest( + queue: Queue.mainQueue(), + context.engine.themes.getChatThemes(accountManager: context.sharedContext.accountManager), + uniqueGiftChatThemesContext.state + |> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in + var peerIds: [EnginePeer.Id] = [] + for theme in state.themes { + if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + peerIds.append(themePeerId) + } + } + return combineLatest( + .single(state), + context.engine.data.get( + EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) + ) + |> map { peers in + var result: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + result[peerId] = peer + } + } + return result + } + ) + } + ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers in + guard let self else { + return + } + self.themes = themes + self.currentUniqueGiftChatThemesState = uniqueGiftChatThemesStateAndPeers.0 + self.uniqueGiftPeers = uniqueGiftChatThemesStateAndPeers.1 + self.rebuildEntries(crossfade: false) + })) + } + + private func hasChanges() -> Bool { + return self.selectedTheme?.id != self.component?.initiallySelectedTheme?.id + } + + private func primaryAction() -> PrimaryAction { + if self.selectedTheme?.id == self.component?.initiallySelectedTheme?.id { + return .chooseWallpaper + } else if self.selectedTheme == nil && self.component?.initiallySelectedTheme != nil { + return .resetTheme + } else { + return .apply + } + } + + private func primaryButtonTitle(strings: PresentationStrings) -> String { + switch self.primaryAction() { + case .chooseWallpaper: + if self.component?.canResetWallpaper == true { + return strings.Conversation_Theme_SetNewPhotoWallpaper + } else { + return strings.Conversation_Theme_SetPhotoWallpaper + } + case .resetTheme: + return strings.Conversation_Theme_Reset + case .apply: + return strings.Conversation_Theme_Apply + } + } + + private func rebuildEntries(crossfade: Bool) { + guard let component = self.component else { + return + } + + let presentationData = component.presentationData + let selectedTheme = self.selectedTheme + let isDarkAppearance = self.isDarkAppearance + var entries: [ThemeSettingsThemeEntry] = [] entries.append(ThemeSettingsThemeEntry( index: 0, @@ -1026,10 +1241,10 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega strings: presentationData.strings, wallpaper: nil )) - - var giftThemes = uniqueGiftChatThemesState.themes + + var giftThemes = self.currentUniqueGiftChatThemesState?.themes ?? [] var existingIds = Set() - if let initiallySelectedTheme, case .gift = initiallySelectedTheme { + if let initiallySelectedTheme = component.initiallySelectedTheme, case .gift = initiallySelectedTheme { let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) if initialThemeIndex == nil || initialThemeIndex! > 50 { giftThemes.insert(initiallySelectedTheme, at: 0) @@ -1048,8 +1263,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega emojiFile = file } } - if let themePeerId = uniqueGift.themePeerId, theme.id != initiallySelectedTheme?.id { - peer = peers[themePeerId] + if let themePeerId = uniqueGift.themePeerId, theme.id != component.initiallySelectedTheme?.id { + peer = self.uniqueGiftPeers[themePeerId] } } let themeReference: PresentationThemeReference @@ -1076,15 +1291,16 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega existingIds.insert(theme.id) } - if uniqueGiftChatThemesState.themes.count == 0 || uniqueGiftChatThemesState.dataState == .ready(canLoadMore: false) { - for theme in themes { + let uniqueGiftThemesState = self.currentUniqueGiftChatThemesState + if uniqueGiftThemesState?.themes.count == 0 || uniqueGiftThemesState?.dataState == .ready(canLoadMore: false) { + for theme in self.themes { guard let emoticon = theme.emoticon else { continue } entries.append(ThemeSettingsThemeEntry( index: entries.count, chatTheme: .emoticon(emoticon), - emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(), + emojiFile: component.animatedEmojiStickers[emoticon]?.first?.file._parse(), themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), peer: nil, nightMode: isDarkAppearance, @@ -1095,438 +1311,649 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega )) } } - + let action: (ChatTheme?) -> Void = { [weak self] chatTheme in - if let self, self.selectedTheme != chatTheme { - self.setChatTheme(chatTheme) - } - } - let previousEntries = strongSelf.entries ?? [] - //let crossfade = previousEntries.count != entries.count - let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: false) - strongSelf.enqueueTransition(transition) - - strongSelf.entries = entries - - if isFirstTime { - for theme in themes { - if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { - let account = strongSelf.context.account - let accountManager = strongSelf.context.sharedContext.accountManager - let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) - if !FileManager.default.fileExists(atPath: path) { - 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, 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)) - - if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { - accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) - } - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedFullSizeDisposable.dispose() - fullSizeDisposable.dispose() - } - } - let _ = accountFullSizeData.start() - } - } - } - } - })) - - self.switchThemeButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity") - strongSelf.animationContainerNode.alpha = 0.4 - } else { - strongSelf.animationContainerNode.alpha = 1.0 - strongSelf.animationContainerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in - guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else { - return - } - if case let .known(value) = offset, value < 100.0 { - self.uniqueGiftChatThemesContext.loadMore() - } - } - - self.updateCancelButton() - } - - deinit { - self.disposable.dispose() - } - - private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { - self.enqueuedTransitions.append(transition) - - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - - private func dequeueTransition() { - guard let transition = self.enqueuedTransitions.first else { - return - } - self.enqueuedTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if self.initialized && transition.crossfade { - options.insert(.AnimateCrossfade) - } - options.insert(.Synchronous) - - var scrollToItem: ListViewScrollToItem? - if !self.initialized { - if let index = transition.entries.firstIndex(where: { entry in - return entry.chatTheme?.id == self.initiallySelectedTheme?.id - }) { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) - self.initialized = true - } - } - - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - - private var skipButtonsUpdate = false - private func setChatTheme(_ chatTheme: ChatTheme?) { - self.animateCrossfade(animateIcon: true) - - self.skipButtonsUpdate = true - self.previewTheme?(chatTheme, self.isDarkAppearance) - self.selectedTheme = chatTheme - let _ = ensureThemeVisible(listNode: self.listNode, themeId: chatTheme?.id, animated: true) - - UIView.transition(with: self.buttonsContentContainerNode.view, duration: ChatThemeScreen.themeCrossfadeDuration, options: [.transitionCrossDissolve, .curveLinear]) { - self.updateButtons() - } - self.updateCancelButton() - self.skipButtonsUpdate = false - - self.themeSelectionsCount += 1 - if self.themeSelectionsCount == 2 { - self.maybePresentPreviewTooltip() - } - } - - private func updateButtons() { - let doneButtonTitle: String - var accentButtonTheme = true - var otherIsEnabled = false - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - otherIsEnabled = self.controller?.canResetWallpaper == true - doneButtonTitle = otherIsEnabled ? self.presentationData.strings.Conversation_Theme_SetNewPhotoWallpaper : self.presentationData.strings.Conversation_Theme_SetPhotoWallpaper - accentButtonTheme = false - } else if self.selectedTheme?.id == nil && self.initiallySelectedTheme?.id != nil { - doneButtonTitle = self.presentationData.strings.Conversation_Theme_Reset - } else { - doneButtonTitle = self.presentationData.strings.Conversation_Theme_Apply - } - - let buttonTheme: SolidRoundedButtonTheme - if accentButtonTheme { - buttonTheme = SolidRoundedButtonTheme(theme: self.presentationData.theme) - } else { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor) - } - UIView.performWithoutAnimation { - self.doneButton.title = doneButtonTitle - self.doneButton.font = accentButtonTheme ? .bold : .regular - } - self.doneButton.updateTheme(buttonTheme) - - self.otherButton.setTitle(self.presentationData.strings.Conversation_Theme_ResetWallpaper, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.destructiveActionTextColor, for: .normal) - self.otherButton.isHidden = !otherIsEnabled - self.textNode.isHidden = !accentButtonTheme || self.controller?.canResetWallpaper == false - - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - - private func updateCancelButton() { - var cancelButtonState: WebAppCancelButtonNode.State = .cancel - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - - } else if self.selectedTheme == nil && self.initiallySelectedTheme != nil { - cancelButtonState = .back - } else { - cancelButtonState = .back - } - self.cancelButtonNode.setState(cancelButtonState, animated: true) - } - - private var switchThemeIconAnimator: DisplayLinkAnimator? - func updatePresentationData(_ presentationData: PresentationData) { - guard !self.animatedOut else { - return - } - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - if let animatingCrossFade = self.animatingCrossFade { - Queue.mainQueue().after(!animatingCrossFade ? ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor() : 0.0, { - self.cancelButtonNode.setTheme(presentationData.theme, animated: true) - }) - } else { - self.cancelButtonNode.setTheme(presentationData.theme, animated: false) - } - - let previousIconColors = iconColors(theme: previousTheme) - let newIconColors = iconColors(theme: self.presentationData.theme) - - if !self.switchThemeButton.isUserInteractionEnabled { - Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor()) { - self.switchThemeIconAnimator = DisplayLinkAnimator(duration: ChatThemeScreen.themeCrossfadeDuration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in - self?.animationNode.setColors(colors: interpolateColors(from: previousIconColors, to: newIconColors, fraction: value)) - }, completion: { [weak self] in - self?.switchThemeIconAnimator?.invalidate() - self?.switchThemeIconAnimator = nil - }) - - UIView.transition(with: self.buttonsContentContainerNode.view, duration: ChatThemeScreen.themeCrossfadeDuration, options: [.transitionCrossDissolve, .curveLinear]) { - self.updateButtons() - } - } - } else { - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: newIconColors) - if !self.skipButtonsUpdate { - self.updateButtons() - } - } - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true - } - - @objc func cancelButtonPressed() { - if self.cancelButtonNode.state == .back { - self.setChatTheme(self.initiallySelectedTheme) - } else { - self.cancel?() - } - } - - @objc func otherButtonPressed() { - if self.selectedTheme?.id != self.initiallySelectedTheme?.id { - self.setChatTheme(self.initiallySelectedTheme) - } else { - if self.controller?.canResetWallpaper == true { - self.controller?.resetWallpaper() - self.cancelButtonPressed() - } else { - self.cancelButtonPressed() - } - } - } - - func complete() { - let proceed = { - self.doneButton.isUserInteractionEnabled = false - self.completion?(self.selectedTheme) - } - if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { + guard let self, self.selectedTheme != chatTheme else { return } - let controller = giftThemeTransferAlertController( - context: self.context, - gift: uniqueGift, - previousPeer: peer, - commit: { - proceed() + self.setChatTheme(chatTheme) + } + let previousEntries = self.entries ?? [] + let transition = preparedTransition(context: component.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + self.enqueueTransition(transition) + + let isFirstTime = self.entries == nil + self.entries = entries + + if isFirstTime { + self.preloadWallpaperResources(themes: self.themes, context: component.context) + } + } + + private func preloadWallpaperResources(themes: [TelegramTheme], context: AccountContext) { + for theme in themes { + if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { + let account = context.account + let accountManager = context.sharedContext.accountManager + let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) + if !FileManager.default.fileExists(atPath: path) { + 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, 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)) + + if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { + accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + let _ = accountFullSizeData.start() } - ) - self.controller?.present(controller, in: .window(.root)) - }) - } else { - proceed() - } - } - - func dimTapped() { - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - self.cancelButtonPressed() - } else { - let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in - if let self { - self.complete() - } - })], actionLayout: .horizontal, dismissOnOutsideTap: true) - self.present?(alertController) - } - } - - @objc func switchThemePressed() { - self.switchThemeButton.isUserInteractionEnabled = false - Queue.mainQueue().after(0.5) { - self.switchThemeButton.isUserInteractionEnabled = true - } - - self.animateCrossfade(animateIcon: false) - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) - Queue.mainQueue().justDispatch { - self.animationNode.playOnce() - } - - let isDarkAppearance = !self.isDarkAppearance - self.previewTheme?(self.selectedTheme, isDarkAppearance) - self.isDarkAppearance = isDarkAppearance - - if isDarkAppearance { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() - } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() - } - } - - private var animatingCrossFade: Bool? - private func animateCrossfade(animateIcon: Bool) { - if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.animationNode.frame - self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.animatingCrossFade = animateIcon - Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor()) { - if let effectView = self.effectNode.view as? UIVisualEffectView { - UIView.animate(withDuration: ChatThemeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { - effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } completion: { _ in } } - - let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatThemeScreen.themeCrossfadeDuration) - - self.animatingCrossFade = nil - } - - if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.contentContainerNode.frame - self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) } - if !animateIcon, let snapshotView = self.otherButton.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.otherButton.frame - self.otherButton.view.superview?.insertSubview(snapshotView, aboveSubview: self.otherButton.view) + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.listNode.forEachVisibleItemNode { node in - if let node = node as? ThemeSettingsThemeItemIconNode { - node.crossfade() + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() } } - } - - private var animatedOut = false - func animateIn() { - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - private var themeSelectionsCount = 0 - private var displayedPreviewTooltip = false - private func maybePresentPreviewTooltip() { - guard !self.displayedPreviewTooltip, !self.animatedOut else { - return + private func dequeueTransition() { + guard let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + if let index = transition.entries.firstIndex(where: { entry in + return entry.chatTheme?.id == self.component?.initiallySelectedTheme?.id + }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) } - let frame = self.switchThemeButton.view.convert(self.switchThemeButton.bounds, to: self.view) - let currentTimestamp = Int32(Date().timeIntervalSince1970) - - let isDark = self.presentationData.theme.overallDarkAppearance - - let signal: Signal<(Int32, Int32), NoError> - if isDark { - signal = ApplicationSpecificNotice.getChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager) - } else { - signal = ApplicationSpecificNotice.getChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager) + private func setChatTheme(_ chatTheme: ChatTheme?) { + guard let component = self.component else { + return + } + + self.previewThemeChanged(chatTheme: chatTheme, dark: self.isDarkAppearance) + self.selectedTheme = chatTheme + self.rebuildEntries(crossfade: false) + let _ = ensureThemeVisible(listNode: self.listNode, themeId: chatTheme?.id, animated: true) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: ChatThemeScreen.themeCrossfadeDuration, curve: .easeInOut))) + + self.themeSelectionsCount += 1 + if self.themeSelectionsCount == 2 { + self.maybePresentPreviewTooltip(component: component) + } } - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { [weak self] count, timestamp in - if let strongSelf = self, count < 2 && currentTimestamp > timestamp + 24 * 60 * 60 { - strongSelf.displayedPreviewTooltip = true + private func previewThemeChanged(chatTheme: ChatTheme?, dark: Bool?) { + self.component?.previewTheme(chatTheme, dark) + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.crossfade() + } + } + } + + private func closeOrBackPressed() { + if self.hasChanges() { + self.setChatTheme(self.component?.initiallySelectedTheme) + } else { + self.component?.cancel() + } + } + + private func resetWallpaperPressed() { + self.component?.resetWallpaper() + self.closeOrBackPressed() + } + + private func primaryPressed() { + switch self.primaryAction() { + case .chooseWallpaper: + self.component?.changeWallpaper() + case .resetTheme, .apply: + self.complete() + } + } + + private func complete() { + guard let component = self.component else { + return + } + let proceed = { [weak self] in + guard let self else { + return + } + self.isCompleting = true + self.state?.updated(transition: .immediate) + component.completion(self.selectedTheme) + } + if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = giftThemeTransferAlertController( + context: component.context, + gift: uniqueGift, + previousPeer: peer, + commit: { + proceed() + } + ) + self.component?.presentInRoot(controller) + }) + } else { + proceed() + } + } + + func dimTapped() { + guard let component = self.component else { + return + } + if !self.hasChanges() { + self.closeOrBackPressed() + } else { + let alertController = textAlertController(context: component.context, updatedPresentationData: (component.presentationData, .single(component.presentationData)), title: nil, text: component.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: component.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: component.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in + self?.complete() + })], actionLayout: .horizontal, dismissOnOutsideTap: true) + component.present(alertController) + } + } + + private func switchThemePressed() { + guard self.isSwitchThemeEnabled, let component = self.component else { + return + } + self.isSwitchThemeEnabled = false + Queue.mainQueue().after(0.5) { [weak self] in + guard let self else { + return + } + self.isSwitchThemeEnabled = true + self.state?.updated(transition: .immediate) + } + + let isDarkAppearance = !self.isDarkAppearance + self.isDarkAppearance = isDarkAppearance + component.previewTheme(self.selectedTheme, isDarkAppearance) + self.rebuildEntries(crossfade: false) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: ChatThemeScreen.themeCrossfadeDuration, curve: .easeInOut))) + Queue.mainQueue().justDispatch { [weak self] in + self?.switchThemePlayOnce.invoke(Void()) + } + + if isDarkAppearance { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() + } else { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() + } + } + + private func maybePresentPreviewTooltip(component: ChatThemeSheetContentComponent) { + guard !self.displayedPreviewTooltip, !self.animatedOut, let switchThemeButtonView = self.switchThemeButton.view else { + return + } + + let frame = switchThemeButtonView.convert(switchThemeButtonView.bounds, to: self) + let currentTimestamp = Int32(Date().timeIntervalSince1970) + let isDark = component.presentationData.theme.overallDarkAppearance + + let signal: Signal<(Int32, Int32), NoError> + if isDark { + signal = ApplicationSpecificNotice.getChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager) + } else { + signal = ApplicationSpecificNotice.getChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager) + } + + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { [weak self] count, timestamp in + guard let self, count < 2 && currentTimestamp > timestamp + 24 * 60 * 60 else { + return + } + self.displayedPreviewTooltip = true - strongSelf.present?(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: isDark ? strongSelf.presentationData.strings.Conversation_Theme_PreviewLightShort : strongSelf.presentationData.strings.Conversation_Theme_PreviewDarkShort), style: .default, icon: nil, location: .point(frame.offsetBy(dx: 3.0, dy: 6.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _, _ in + component.present(TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .plain(text: isDark ? component.presentationData.strings.Conversation_Theme_PreviewLightShort : component.presentationData.strings.Conversation_Theme_PreviewDarkShort), style: .default, icon: nil, location: .point(frame.offsetBy(dx: 3.0, dy: 6.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) })) if isDark { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + } + }) + } + + func update(component: ChatThemeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + let environmentValue = environment[EnvironmentType.self].value + self.environment = environmentValue + + self.setupIfNeeded(component: component) + + if let previousComponent, previousComponent.presentationData.theme !== component.presentationData.theme || previousComponent.presentationData.strings !== component.presentationData.strings { + self.rebuildEntries(crossfade: false) + } + + let width = availableSize.width + let topInset: CGFloat = 16.0 + let titleBarHeight: CGFloat = 44.0 + let sideInset: CGFloat = 16.0 + let buttonHeight: CGFloat = 52.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environmentValue.safeInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) + let bottomInset: CGFloat = environmentValue.safeInsets.bottom.isZero ? 24.0 : environmentValue.safeInsets.bottom + 10.0 + + var contentHeight = topInset + + let leftButtonIconName = self.hasChanges() ? "Navigation/Back" : "Navigation/Close" + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: component.presentationData.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: leftButtonIconName, component: AnyComponent( + BundleIconComponent( + name: leftButtonIconName, + tintColor: component.presentationData.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.closeOrBackPressed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: leftButtonSize)) + } + + let switchButtonSize = self.switchThemeButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: component.presentationData.theme.overallDarkAppearance, + state: .glass, + isEnabled: self.isSwitchThemeEnabled, + component: AnyComponentWithIdentity(id: self.isDarkAppearance ? "night" : "day", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun"), + color: component.presentationData.theme.chat.inputPanel.panelControlColor, + startingPosition: .end, + size: CGSize(width: 28.0, height: 28.0), + playOnce: self.switchThemePlayOnce + ) + )), + action: { [weak self] _ in + self?.switchThemePressed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let switchButtonView = self.switchThemeButton.view { + if switchButtonView.superview == nil { + self.addSubview(switchButtonView) + } + transition.setFrame(view: switchButtonView, frame: CGRect(origin: CGPoint(x: width - sideInset - switchButtonSize.width, y: topInset), size: switchButtonSize)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: component.presentationData.strings.Conversation_Theme_Title, + font: Font.semibold(17.0), + color: component.presentationData.theme.actionSheet.primaryTextColor + )), + environment: {}, + containerSize: CGSize(width: width - 120.0, height: titleBarHeight) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) * 0.5), y: topInset + floorToScreenPixels((titleBarHeight - titleSize.height) * 0.5)), size: titleSize)) + } + + contentHeight += titleBarHeight + 8.0 + + let listHeight: CGFloat = 120.0 + let listFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: listHeight)) + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: listHeight, height: width) + self.listNode.position = CGPoint(x: listFrame.midX, y: listFrame.midY) + var listInsets = UIEdgeInsets() + listInsets.top += environmentValue.safeInsets.left + 12.0 + listInsets.bottom += environmentValue.safeInsets.right + 12.0 + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + contentHeight += listHeight + 12.0 + + let showSubtitle = self.primaryAction() != .chooseWallpaper && component.canResetWallpaper + if showSubtitle { + let subtitleSize = self.subtitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.presentationData.strings.Conversation_Theme_Subtitle(component.peerName).string, font: Font.regular(15.0), textColor: component.presentationData.theme.actionSheet.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - 90.0, height: 100.0) + ) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setAlpha(view: subtitleView, alpha: 1.0) + transition.setFrame(view: subtitleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)) + } + contentHeight += subtitleSize.height + 12.0 + } else if let subtitleView = self.subtitle.view { + transition.setAlpha(view: subtitleView, alpha: 0.0) + } + + let primaryAction = self.primaryAction() + let accentColor = component.presentationData.theme.actionSheet.controlAccentColor + let primaryTextColor: UIColor + let primaryBackground: ButtonComponent.Background + switch primaryAction { + case .chooseWallpaper: + primaryTextColor = accentColor + primaryBackground = ButtonComponent.Background( + style: .glass, + color: accentColor.withMultipliedAlpha(0.1), + foreground: accentColor, + pressedColor: accentColor.withMultipliedAlpha(0.2), + cornerRadius: 26.0 + ) + case .resetTheme, .apply: + primaryTextColor = component.presentationData.theme.list.itemCheckColors.foregroundColor + primaryBackground = ButtonComponent.Background( + style: .glass, + color: component.presentationData.theme.list.itemCheckColors.fillColor, + foreground: component.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 26.0 + ) + } + + let primaryButtonSize = self.primaryButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: primaryBackground, + content: AnyComponentWithIdentity( + id: AnyHashable(self.primaryButtonTitle(strings: component.presentationData.strings)), + component: AnyComponent(ButtonTextContentComponent( + text: self.primaryButtonTitle(strings: component.presentationData.strings), + badge: 0, + textColor: primaryTextColor, + badgeBackground: primaryTextColor, + badgeForeground: component.presentationData.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: !self.isCompleting, + displaysProgress: self.isCompleting, + action: { [weak self] in + self?.primaryPressed() + } + )), + environment: {}, + containerSize: CGSize(width: width - buttonInsets.left - buttonInsets.right, height: buttonHeight) + ) + if let primaryButtonView = self.primaryButton.view { + if primaryButtonView.superview == nil { + self.addSubview(primaryButtonView) + } + transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: primaryButtonSize)) + } + contentHeight += primaryButtonSize.height + + let showResetWallpaper = component.canResetWallpaper && primaryAction == .chooseWallpaper + if showResetWallpaper { + contentHeight += 8.0 + let resetWallpaperButton: ComponentView + if let current = self.resetWallpaperButton { + resetWallpaperButton = current + } else { + resetWallpaperButton = ComponentView() + self.resetWallpaperButton = resetWallpaperButton + } + + let destructiveColor = component.presentationData.theme.actionSheet.destructiveActionTextColor + let resetButtonSize = resetWallpaperButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: destructiveColor.withMultipliedAlpha(0.1), + foreground: destructiveColor, + pressedColor: destructiveColor.withMultipliedAlpha(0.2), + cornerRadius: 22.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable("resetWallpaper"), + component: AnyComponent(ButtonTextContentComponent( + text: component.presentationData.strings.Conversation_Theme_ResetWallpaper, + badge: 0, + textColor: destructiveColor, + badgeBackground: destructiveColor, + badgeForeground: component.presentationData.theme.actionSheet.itemBackgroundColor + )) + ), + action: { [weak self] in + self?.resetWallpaperPressed() + } + )), + environment: {}, + containerSize: CGSize(width: width - buttonInsets.left - buttonInsets.right, height: buttonHeight) + ) + if let resetButtonView = resetWallpaperButton.view { + if resetButtonView.superview == nil { + self.addSubview(resetButtonView) + } + transition.setAlpha(view: resetButtonView, alpha: 1.0) + transition.setFrame(view: resetButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: resetButtonSize)) + } + contentHeight += resetButtonSize.height + } else if let resetWallpaperButton = self.resetWallpaperButton { + self.resetWallpaperButton = nil + if let resetButtonView = resetWallpaperButton.view { + transition.setAlpha(view: resetButtonView, alpha: 0.0, completion: { _ in + resetButtonView.removeFromSuperview() + }) } } - }) + + contentHeight += bottomInset + + return CGSize(width: width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatThemeSheetScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: ChatThemeScreen? + + private let animatedEmojiStickers: [String: [StickerPackItem]] + private let initiallySelectedTheme: ChatTheme? + private let peerName: String + + private let hostView: ComponentHostView + private var containerLayout: (ContainerViewLayout, CGFloat)? + private var isDisplaying = false + private var animatedOut = false + + var present: ((ViewController) -> Void)? + var previewTheme: ((ChatTheme?, Bool?) -> Void)? + var completion: ((ChatTheme?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + var passthroughHitTestImpl: ((CGPoint) -> UIView?)? + + init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedTheme: ChatTheme?, peerName: String) { + self.context = context + self.presentationData = presentationData + self.controller = controller + self.animatedEmojiStickers = animatedEmojiStickers + self.initiallySelectedTheme = initiallySelectedTheme + self.peerName = peerName + self.hostView = ComponentHostView() + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + self.view.addSubview(self.hostView) + } + + private func update(transition: ComponentTransition) { + guard let (layout, navigationBarHeight) = self.containerLayout else { + return + } + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: navigationBarHeight, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: layout.metrics.orientation, + isVisible: self.isDisplaying, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let component = ChatThemeScreenComponent( + context: self.context, + presentationData: self.presentationData, + animatedEmojiStickers: self.animatedEmojiStickers, + initiallySelectedTheme: self.initiallySelectedTheme, + peerName: self.peerName, + canResetWallpaper: self.controller?.canResetWallpaper == true, + present: { [weak self] controller in + self?.present?(controller) + }, + presentInRoot: { [weak self] controller in + self?.controller?.present(controller, in: .window(.root)) + }, + previewTheme: { [weak self] chatTheme, dark in + self?.previewTheme?(chatTheme, dark) + }, + changeWallpaper: { [weak self] in + self?.controller?.changeWallpaper() + }, + resetWallpaper: { [weak self] in + self?.controller?.resetWallpaper() + }, + completion: { [weak self] chatTheme in + self?.completion?(chatTheme) + }, + cancel: { [weak self] in + self?.cancel?() + } + ) + + let _ = self.hostView.update( + transition: transition, + component: AnyComponent(component), + environment: { + environment + }, + containerSize: layout.size + ) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + func updatePresentationData(_ presentationData: PresentationData) { + guard !self.animatedOut else { + return + } + self.presentationData = presentationData + self.update(transition: .immediate) + } + + func animateIn() { + self.isDisplaying = true + self.update(transition: ComponentTransition(animation: .none).withUserData(ViewControllerComponentContainer.AnimateInTransition())) } func animateOut(completion: (() -> Void)? = nil) { self.animatedOut = true - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.dismiss?() + if let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View { + rootView.animateOut { [weak self] in + self?.dismiss?() completion?() } - }) + } else { + self.dismiss?() + completion?() + } + } + + func dimTapped() { + if let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View { + rootView.dimTapped() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + self.update(transition: ComponentTransition(transition)) } - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { var presentingAlertController = false self.controller?.forEachController({ c in @@ -1536,108 +1963,13 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega return true }) - if !presentingAlertController && self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - if let result = self.passthroughHitTestImpl?(point) { - return result - } else { - return nil - } + if !presentingAlertController && self.bounds.contains(point), let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View, !rootView.containsContent(point: point) { + if let result = self.passthroughHitTestImpl?(point) { + return result + } else { + return nil } } return super.hitTest(point, with: event) } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 168.0 - if self.controller?.canResetWallpaper == true { - contentHeight += 50.0 - } - if cleanInsets.bottom.isZero { - insets.bottom += 14.0 - contentHeight += 14.0 - } - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0 + UIScreenPixel), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let switchThemeSize = CGSize(width: 44.0, height: 44.0) - let switchThemeFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeSize.width - 3.0, y: 6.0), size: switchThemeSize) - transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) - transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: .zero, size: self.animationContainerNode.frame.size)) - - let cancelSize = self.cancelButtonNode.calculateSizeThatFits(CGSize(width: layout.size.width, height: 56.0)) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: cancelSize) - transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - - let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInsets.left - buttonInsets.right, transition: transition) - var doneY = contentHeight - doneButtonHeight - 2.0 - insets.bottom - if self.controller?.canResetWallpaper == true { - doneY = contentHeight - doneButtonHeight - 52.0 - insets.bottom - } - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInsets.left, y: doneY, width: contentFrame.width, height: doneButtonHeight)) - - let otherButtonSize = self.otherButton.measure(CGSize(width: contentFrame.width - buttonInset * 2.0, height: .greatestFiniteMagnitude)) - self.otherButton.frame = CGRect(origin: CGPoint(x: floor((contentFrame.width - otherButtonSize.width) / 2.0), y: contentHeight - otherButtonSize.height - insets.bottom - 15.0), size: otherButtonSize) - - let textSize = self.textNode.updateLayout(CGSize(width: width - 90.0, height: titleHeight)) - let textFrame: CGRect - if self.controller?.canResetWallpaper == true { - textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: contentHeight - textSize.height - insets.bottom - 17.0), size: textSize) - } else { - textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: contentHeight - textSize.height - insets.bottom - 15.0), size: textSize) - } - transition.updateFrame(node: self.textNode, frame: textFrame) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.buttonsContentContainerNode, frame: contentContainerFrame) - - var listInsets = UIEdgeInsets() - listInsets.top += layout.safeInsets.left + 12.0 - listInsets.bottom += layout.safeInsets.right + 12.0 - - let contentSize = CGSize(width: contentFrame.width, height: 120.0) - - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) - self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight - 4.0) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } } diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 5eb51a3f78..08b1aeff36 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -318,6 +318,32 @@ final class ComposeTodoScreenComponent: Component { } } + private static func limitedAttributedString(_ text: NSAttributedString, characterLimit: Int) -> NSAttributedString { + if text.string.count <= characterLimit { + return text + } + + let string = text.string + let endIndex = string.index(string.startIndex, offsetBy: characterLimit) + let length = NSRange(string.startIndex ..< endIndex, in: string).length + return text.attributedSubstring(from: NSRange(location: 0, length: length)) + } + + private static func attributedTodoItemLines(from text: NSAttributedString, characterLimit: Int) -> [NSAttributedString] { + var result: [NSAttributedString] = [] + + text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byLines, .substringNotRequired]) { _, substringRange, _, _ in + let range = NSRange(substringRange, in: text.string) + let line = text.attributedSubstring(from: range) + if line.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return + } + result.append(Self.limitedAttributedString(line, characterLimit: characterLimit)) + } + + return result + } + func validatedInput() -> TelegramMediaTodo? { if self.todoTextInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { return nil @@ -328,19 +354,10 @@ final class ComposeTodoScreenComponent: Component { if todoItem.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { continue } - var entities: [MessageTextEntity] = [] - for entity in generateChatInputTextEntities(todoItem.textInputState.text) { - switch entity.type { - case .CustomEmoji: - entities.append(entity) - default: - break - } - } mappedItems.append( TelegramMediaTodo.Item( text: todoItem.textInputState.text.string, - entities: entities, + entities: generateChatInputTextEntities(todoItem.textInputState.text), id: todoItem.id ) ) @@ -350,16 +367,6 @@ final class ComposeTodoScreenComponent: Component { return nil } - var textEntities: [MessageTextEntity] = [] - for entity in generateChatInputTextEntities(self.todoTextInputState.text) { - switch entity.type { - case .CustomEmoji: - textEntities.append(entity) - default: - break - } - } - var flags: TelegramMediaTodo.Flags = [] if self.isCompletableByOthers { flags.insert(.othersCanComplete) @@ -371,7 +378,7 @@ final class ComposeTodoScreenComponent: Component { return TelegramMediaTodo( flags: flags, text: self.todoTextInputState.text.string, - textEntities: textEntities, + textEntities: generateChatInputTextEntities(self.todoTextInputState.text), items: mappedItems ) } @@ -792,6 +799,7 @@ final class ComposeTodoScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag, characterLimit: component.initialData.maxTodoTextLength, + formattingAvailable: true, emptyLineHandling: .allowed, returnKeyAction: { [weak self] in guard let self else { @@ -884,6 +892,7 @@ final class ComposeTodoScreenComponent: Component { hasLeftInset: true, canReorder: isEnabled, canAdd: isEnabled && i != 0 && i < component.initialData.maxTodoItemsCount, + formattingAvailable: true, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { @@ -945,23 +954,19 @@ final class ComposeTodoScreenComponent: Component { return } if case let .text(text) = data { - let lines = text.string.components(separatedBy: "\n") + let lines = ComposeTodoScreenComponent.View.attributedTodoItemLines(from: text, characterLimit: component.initialData.maxTodoItemLength) if !lines.isEmpty { self.endEditing(true) var i = 0 for line in lines { - if line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - continue - } - let line = String(line.prefix(component.initialData.maxTodoItemLength)) if i < self.todoItems.count { - self.todoItems[i].resetText = NSAttributedString(string: line) + self.todoItems[i].resetText = line } else { if self.todoItems.count < component.initialData.maxTodoItemsCount { let todoItem = ComposeTodoScreenComponent.TodoItem( id: self.nextTodoItemId ) - todoItem.resetText = NSAttributedString(string: line) + todoItem.resetText = line self.todoItems.append(todoItem) self.nextTodoItemId += 1 } @@ -1294,7 +1299,14 @@ final class ComposeTodoScreenComponent: Component { component: AnyComponent(ListSectionComponent( theme: theme, style: .glass, - header: nil, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_SettingsTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), footer: nil, items: todoSettingsSectionItems )), diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift index 475a4d4f54..8eac5fce4c 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift @@ -246,7 +246,7 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { self.phoneInputNode.number = "+\(countryCodeAndId.0)" } - func processNumberChange(_ number: String) -> Bool { + func processNumberChange(_ number: String, notifyUpdated: Bool = true) -> Bool { guard let item = self.item else { return false } @@ -270,7 +270,9 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { self.phoneInputNode.mask = nil self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) } - item.updated(number, rawMask) + if notifyUpdated { + item.updated(number, rawMask) + } return true } else { @@ -297,7 +299,10 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { } func updateCountryCode(code: Int32, name: String, phoneNumber: String? = nil) { - self.phoneInputNode.codeAndNumber = (code, name, phoneNumber ?? self.phoneInputNode.codeAndNumber.2) + let targetNumber = phoneNumber ?? self.phoneInputNode.codeAndNumber.2 + let targetFullNumber = "+\(code)\(targetNumber)" + let _ = self.processNumberChange(targetFullNumber, notifyUpdated: false) + self.phoneInputNode.codeAndNumber = (code, name, targetNumber) let _ = self.processNumberChange(self.phoneInputNode.number) } diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift index 40aa79c5bf..93924c3791 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift @@ -738,6 +738,7 @@ final class ContextActionsContainerNode: ASDisplayNode { if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift index 1bc22b48f5..940e6132f1 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift @@ -152,6 +152,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false self.contentContainerNode = ContextContentContainerNode() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 1c0a044379..d82defe799 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -40,7 +40,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/UndoUI:UndoUI", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", - "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/LocalizedPeerData:LocalizedPeerData", "//submodules/TelegramNotices:TelegramNotices", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 59b3223227..7e8a44a1a6 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -19,7 +19,8 @@ import StickerResources import AppBundle import UndoUI import AudioToolbox -import SolidRoundedButtonComponent +import ButtonComponent +import LottieComponent import EmojiTextAttachmentView import EmojiStatusComponent import TelegramNotices @@ -3354,23 +3355,40 @@ public final class EmojiPagerContentComponent: Component { gloss = false } + var groupPremiumButtonItems: [AnyComponentWithIdentity] = [] + groupPremiumButtonItems.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(ButtonTextContentComponent( + text: title, + badge: 0, + textColor: foregroundColor, + badgeBackground: foregroundColor, + badgeForeground: backgroundColor + )))) + if let animationName { + groupPremiumButtonItems.append(AnyComponentWithIdentity(id: "animation", component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: animationName), + color: foregroundColor, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } + let groupPremiumButtonSize = groupPremiumButton.update( transition: groupPremiumButtonTransition, - component: AnyComponent(SolidRoundedButtonComponent( - title: title, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: backgroundColor, - backgroundColors: backgroundColors, - foregroundColor: foregroundColor + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: backgroundColor, + foreground: foregroundColor, + pressedColor: backgroundColor.withMultipliedAlpha(0.8), + cornerRadius: groupBorderRadius, + isShimmering: gloss, + gradient: backgroundColors.count > 1 ? ButtonComponent.Background.Gradient(colors: backgroundColors) : nil + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(title)-\(animationName ?? "")"), + component: AnyComponent(HStack(groupPremiumButtonItems, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: groupBorderRadius, - gloss: gloss, - animationName: animationName, - iconPosition: .right, - iconSpacing: 4.0, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index 933d7da6bc..4a240c11c0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -248,6 +248,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 040cc0c9de..39fa7c4fc4 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -270,12 +270,141 @@ public class GlassBackgroundView: UIView { } } + public struct CornerRadii: Equatable { + public let topLeft: CGFloat + public let topRight: CGFloat + public let bottomLeft: CGFloat + public let bottomRight: CGFloat + + public init(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + + public init(radius: CGFloat) { + self.init(topLeft: radius, topRight: radius, bottomLeft: radius, bottomRight: radius) + } + + fileprivate func insetBy(_ value: CGFloat) -> CornerRadii { + return CornerRadii( + topLeft: max(0.0, self.topLeft - value), + topRight: max(0.0, self.topRight - value), + bottomLeft: max(0.0, self.bottomLeft - value), + bottomRight: max(0.0, self.bottomRight - value) + ) + } + + fileprivate var maximum: CGFloat { + return max(max(self.topLeft, self.topRight), max(self.bottomLeft, self.bottomRight)) + } + } + public enum Shape: Equatable { case roundedRect(cornerRadius: CGFloat) + case customRoundedRect(cornerRadii: CornerRadii) + + fileprivate func cornerRadii(for size: CGSize) -> CornerRadii { + switch self { + case let .roundedRect(cornerRadius): + return GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: CornerRadii(radius: cornerRadius)) + case let .customRoundedRect(cornerRadii): + return GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii) + } + } + + func maximumCornerRadius(for size: CGSize) -> CGFloat { + return self.cornerRadii(for: size).maximum + } + } + + static func clampedCornerRadii(size: CGSize, cornerRadii: CornerRadii) -> CornerRadii { + let size = CGSize(width: max(0.0, size.width), height: max(0.0, size.height)) + var cornerRadii = CornerRadii( + topLeft: max(0.0, cornerRadii.topLeft), + topRight: max(0.0, cornerRadii.topRight), + bottomLeft: max(0.0, cornerRadii.bottomLeft), + bottomRight: max(0.0, cornerRadii.bottomRight) + ) + + func scaleFor(edgeLength: CGFloat, _ lhs: CGFloat, _ rhs: CGFloat) -> CGFloat { + let sum = lhs + rhs + if sum <= edgeLength || sum.isZero { + return 1.0 + } + return edgeLength / sum + } + + let scale = min( + 1.0, + scaleFor(edgeLength: size.width, cornerRadii.topLeft, cornerRadii.topRight), + scaleFor(edgeLength: size.width, cornerRadii.bottomLeft, cornerRadii.bottomRight), + scaleFor(edgeLength: size.height, cornerRadii.topLeft, cornerRadii.bottomLeft), + scaleFor(edgeLength: size.height, cornerRadii.topRight, cornerRadii.bottomRight) + ) + + if scale < 1.0 { + cornerRadii = CornerRadii( + topLeft: cornerRadii.topLeft * scale, + topRight: cornerRadii.topRight * scale, + bottomLeft: cornerRadii.bottomLeft * scale, + bottomRight: cornerRadii.bottomRight * scale + ) + } + + return cornerRadii + } + + static func generateRoundedRectPath(rect: CGRect, cornerRadii: CornerRadii) -> CGPath { + let cornerRadii = self.clampedCornerRadii(size: rect.size, cornerRadii: cornerRadii) + let path = CGMutablePath() + + func addCorner(tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat) { + if radius > CGFloat.ulpOfOne { + path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: radius) + } else { + path.addLine(to: tangent1End) + path.addLine(to: tangent2End) + } + } + + path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadii.topLeft)) + addCorner( + tangent1End: CGPoint(x: rect.minX, y: rect.minY), + tangent2End: CGPoint(x: rect.minX + cornerRadii.topLeft, y: rect.minY), + radius: cornerRadii.topLeft + ) + path.addLine(to: CGPoint(x: rect.maxX - cornerRadii.topRight, y: rect.minY)) + addCorner( + tangent1End: CGPoint(x: rect.maxX, y: rect.minY), + tangent2End: CGPoint(x: rect.maxX, y: rect.minY + cornerRadii.topRight), + radius: cornerRadii.topRight + ) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadii.bottomRight)) + addCorner( + tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), + tangent2End: CGPoint(x: rect.maxX - cornerRadii.bottomRight, y: rect.maxY), + radius: cornerRadii.bottomRight + ) + path.addLine(to: CGPoint(x: rect.minX + cornerRadii.bottomLeft, y: rect.maxY)) + addCorner( + tangent1End: CGPoint(x: rect.minX, y: rect.maxY), + tangent2End: CGPoint(x: rect.minX, y: rect.maxY - cornerRadii.bottomLeft), + radius: cornerRadii.bottomLeft + ) + path.closeSubpath() + + return path + } + + static func generateRoundedRectPath(size: CGSize, cornerRadii: CornerRadii) -> CGPath { + return self.generateRoundedRectPath(rect: CGRect(origin: CGPoint(), size: size), cornerRadii: cornerRadii) } private final class ClippingShapeContext { let view: UIView + private var maskLayer: CAShapeLayer? private(set) var shape: Shape? @@ -288,7 +417,33 @@ public class GlassBackgroundView: UIView { switch shape { case let .roundedRect(cornerRadius): + self.maskLayer = nil + self.view.layer.mask = nil transition.setCornerRadius(layer: self.view.layer, cornerRadius: cornerRadius) + case let .customRoundedRect(cornerRadii): + transition.setCornerRadius(layer: self.view.layer, cornerRadius: 0.0) + if #available(iOS 26.0, *) { + transition.animateView { + self.view.cornerConfiguration = .corners( + topLeftRadius: .fixed(cornerRadii.topLeft), + topRightRadius: .fixed(cornerRadii.topRight), + bottomLeftRadius: .fixed(cornerRadii.bottomLeft), + bottomRightRadius: .fixed(cornerRadii.bottomRight) + ) + } + } else { + let maskLayer: CAShapeLayer + if let current = self.maskLayer { + maskLayer = current + } else { + maskLayer = CAShapeLayer() + maskLayer.fillColor = UIColor.black.cgColor + self.maskLayer = maskLayer + self.view.layer.mask = maskLayer + } + transition.setFrame(layer: maskLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setShapeLayerPath(layer: maskLayer, path: GlassBackgroundView.generateRoundedRectPath(size: size, cornerRadii: cornerRadii)) + } } } } @@ -311,6 +466,7 @@ public class GlassBackgroundView: UIView { private let legacyView: LegacyGlassView? private let legacyHighlightContainerView: UIView? + private let legacyHighlightClippingContext: ClippingShapeContext? private var glassHighlightRecognizer: GlassHighlightGestureRecognizer? private let nativeView: UIVisualEffectView? @@ -342,6 +498,7 @@ public class GlassBackgroundView: UIView { if #available(iOS 26.0, *), !GlassBackgroundView.useCustomGlassImpl { self.legacyView = nil self.legacyHighlightContainerView = nil + self.legacyHighlightClippingContext = nil let glassEffect = UIGlassEffect(style: .regular) glassEffect.isInteractive = false @@ -362,6 +519,7 @@ public class GlassBackgroundView: UIView { legacyHighlightContainerView.isUserInteractionEnabled = false legacyHighlightContainerView.clipsToBounds = true self.legacyHighlightContainerView = legacyHighlightContainerView + self.legacyHighlightClippingContext = ClippingShapeContext(view: legacyHighlightContainerView) self.nativeView = nil self.nativeViewClippingContext = nil self.nativeParamsView = nil @@ -438,13 +596,21 @@ public class GlassBackgroundView: UIView { public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { let shape: Shape = .roundedRect(cornerRadius: cornerRadius) + self.update(size: size, shape: shape, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible, transition: transition) + } + + public func update(size: CGSize, cornerRadii: CornerRadii, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { + let shape: Shape = .customRoundedRect(cornerRadii: cornerRadii) + self.update(size: size, shape: shape, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible, transition: transition) + } + + func update(size: CGSize, shape: Shape, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { if let glassHighlightRecognizer = self.glassHighlightRecognizer { glassHighlightRecognizer.isEnabled = isInteractive } - if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) { - + if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape || (nativeView.overrideUserInterfaceStyle == .dark) != isDark) { nativeViewClippingContext.update(shape: shape, size: size, transition: transition) if transition.animation.isImmediate { nativeView.frame = CGRect(origin: CGPoint(), size: size) @@ -457,24 +623,21 @@ public class GlassBackgroundView: UIView { nativeView.overrideUserInterfaceStyle = isDark ? .dark : .light } if let legacyView = self.legacyView { - switch shape { - case let .roundedRect(cornerRadius): - let style: LegacyGlassView.Style - switch tintColor.kind { - case .panel: - style = .normal + let style: LegacyGlassView.Style + switch tintColor.kind { + case .panel: + style = .normal + case .clear: + style = .clear + case let .custom(styleValue, _): + switch styleValue { case .clear: style = .clear - case let .custom(styleValue, _): - switch styleValue { - case .clear: - style = .clear - case .default: - style = .normal - } + case .default: + style = .normal } - legacyView.update(size: size, cornerRadius: cornerRadius, style: style, transition: transition) } + legacyView.update(size: size, shape: shape, style: style, transition: transition) transition.setFrame(view: legacyView, frame: CGRect(origin: CGPoint(), size: size)) transition.setAlpha(view: legacyView, alpha: isVisible ? 1.0 : 0.0) @@ -483,10 +646,7 @@ public class GlassBackgroundView: UIView { } if let legacyHighlightContainerView = self.legacyHighlightContainerView { transition.setFrame(view: legacyHighlightContainerView, frame: CGRect(origin: CGPoint(), size: size)) - switch shape { - case let .roundedRect(cornerRadius): - transition.setCornerRadius(layer: legacyHighlightContainerView.layer, cornerRadius: cornerRadius) - } + self.legacyHighlightClippingContext?.update(shape: shape, size: size, transition: transition) } let shadowInset: CGFloat = 32.0 @@ -534,14 +694,13 @@ public class GlassBackgroundView: UIView { if self.params != params { self.params = params - let outerCornerRadius: CGFloat - switch shape { - case let .roundedRect(cornerRadius): - outerCornerRadius = cornerRadius - } - if let shadowView = self.shadowView { - shadowView.image = Self.generateLegacyShadowImage(cornerRadius: outerCornerRadius, shadowInset: shadowInset) + switch shape { + case let .roundedRect(cornerRadius): + shadowView.image = Self.generateLegacyShadowImage(cornerRadius: cornerRadius, shadowInset: shadowInset) + case let .customRoundedRect(cornerRadii): + shadowView.image = Self.generateLegacyShadowImage(cornerRadii: GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii), shadowInset: shadowInset) + } transition.setAlpha(view: shadowView, alpha: isVisible ? 1.0 : 0.0) } @@ -568,7 +727,12 @@ public class GlassBackgroundView: UIView { borderWidthFactor = 1.0 } } - foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: outerCornerRadius * 2.0, height: outerCornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + switch shape { + case let .roundedRect(cornerRadius): + foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + case let .customRoundedRect(cornerRadii): + foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(cornerRadii: GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + } #if DEBUG //foregroundView.image = nil #endif @@ -606,7 +770,7 @@ public class GlassBackgroundView: UIView { glassEffectValue.tintColor = nil } } - glassEffectValue.isInteractive = params.isInteractive + glassEffectValue.isInteractive = isInteractive glassEffect = glassEffectValue } @@ -855,6 +1019,33 @@ private extension CGContext { } } +private struct LegacyResizableImageMetrics { + let imageSize: CGSize + let innerRect: CGRect + let leftCapWidth: Int + let topCapHeight: Int +} + +private func legacyResizableImageMetrics(cornerRadii: GlassBackgroundView.CornerRadii, inset: CGFloat) -> LegacyResizableImageMetrics { + let leftRadius = ceil(max(cornerRadii.topLeft, cornerRadii.bottomLeft)) + let rightRadius = ceil(max(cornerRadii.topRight, cornerRadii.bottomRight)) + let topRadius = ceil(max(cornerRadii.topLeft, cornerRadii.topRight)) + let bottomRadius = ceil(max(cornerRadii.bottomLeft, cornerRadii.bottomRight)) + + let innerSize = CGSize( + width: max(1.0, leftRadius + rightRadius + 1.0), + height: max(1.0, topRadius + bottomRadius + 1.0) + ) + let imageSize = CGSize(width: innerSize.width + inset * 2.0, height: innerSize.height + inset * 2.0) + + return LegacyResizableImageMetrics( + imageSize: imageSize, + innerRect: CGRect(origin: CGPoint(x: inset, y: inset), size: innerSize), + leftCapWidth: Int(ceil(inset + leftRadius)), + topCapHeight: Int(ceil(inset + topRadius)) + ) +} + public extension GlassBackgroundView { static func generateLegacyShadowImage(cornerRadius: CGFloat, shadowInset: CGFloat = 32.0, shadowIntensity: CGFloat = 0.04, shadowBlur: CGFloat = 40.0) -> UIImage? { let shadowInnerInset: CGFloat = 0.5 @@ -884,6 +1075,31 @@ public extension GlassBackgroundView { topCapHeight: Int(shadowInset + cornerRadius) ) } + + static func generateLegacyShadowImage(cornerRadii: CornerRadii, shadowInset: CGFloat = 32.0, shadowIntensity: CGFloat = 0.04, shadowBlur: CGFloat = 40.0) -> UIImage? { + let shadowInnerInset: CGFloat = 0.5 + let metrics = legacyResizableImageMetrics(cornerRadii: cornerRadii, inset: shadowInset) + + return generateImage(metrics.imageSize, rotatedContext: { _, context in + context.clear(CGRect(origin: CGPoint(), size: metrics.imageSize)) + + let shadowRect = metrics.innerRect.insetBy(dx: shadowInnerInset, dy: shadowInnerInset) + let shadowPath = GlassBackgroundView.generateRoundedRectPath(rect: shadowRect, cornerRadii: cornerRadii) + + context.setFillColor(UIColor.black.cgColor) + context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: shadowBlur, color: UIColor(white: 0.0, alpha: shadowIntensity).cgColor) + context.addPath(shadowPath) + context.fillPath() + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.addPath(shadowPath) + context.fillPath() + })?.stretchableImage( + withLeftCapWidth: metrics.leftCapWidth, + topCapHeight: metrics.topCapHeight + ) + } static func generateLegacyGlassImage(size: CGSize, inset: CGFloat, borderWidthFactor: CGFloat = 1.0, isDark: Bool, fillColor: UIColor) -> UIImage { var size = size @@ -1078,6 +1294,106 @@ public extension GlassBackgroundView { }.stretchableImage(withLeftCapWidth: Int(size.width * 0.5), topCapHeight: Int(size.height * 0.5)) } + static func generateLegacyGlassImage(cornerRadii: CornerRadii, inset: CGFloat, borderWidthFactor: CGFloat = 1.0, isDark: Bool, fillColor: UIColor) -> UIImage { + let metrics = legacyResizableImageMetrics(cornerRadii: cornerRadii, inset: inset) + let size = metrics.imageSize + let innerRect = metrics.innerRect + + return UIGraphicsImageRenderer(size: size).image { ctx in + let context = ctx.cgContext + + context.clear(CGRect(origin: CGPoint(), size: size)) + + let addShadow: (CGContext, Bool, CGPoint, CGFloat, CGFloat, UIColor, CGBlendMode) -> Void = { context, isOuter, position, blur, spread, shadowColor, blendMode in + var blur = blur + + if isOuter { + blur += abs(spread) + + context.beginTransparencyLayer(auxiliaryInfo: nil) + context.saveGState() + defer { + context.restoreGState() + context.endTransparencyLayer() + } + + let spreadRect = innerRect.insetBy(dx: 0.25, dy: 0.25) + let spreadPath = GlassBackgroundView.generateRoundedRectPath(rect: spreadRect, cornerRadii: cornerRadii) + + context.setShadow(offset: CGSize(width: position.x, height: position.y), blur: blur, color: shadowColor.cgColor) + context.setFillColor(UIColor.black.withAlphaComponent(1.0).cgColor) + context.addPath(spreadPath) + context.fillPath() + + let cleanPath = GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.addPath(cleanPath) + context.fillPath() + context.setBlendMode(.normal) + } else { + let image = UIGraphicsImageRenderer(size: size).image(actions: { ctx in + let context = ctx.cgContext + let spreadRect = innerRect.insetBy(dx: -spread - 0.33, dy: -spread - 0.33) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: position.x, height: position.y), blur: blur, color: shadowColor.cgColor) + context.setFillColor(shadowColor.cgColor) + context.addPath(UIBezierPath(rect: spreadRect.insetBy(dx: -10000.0, dy: -10000.0)).cgPath) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: spreadRect, cornerRadii: cornerRadii)) + context.fillPath(using: .evenOdd) + }) + + UIGraphicsPushContext(context) + image.draw(in: CGRect(origin: .zero, size: size), blendMode: blendMode, alpha: 1.0) + UIGraphicsPopContext() + } + } + + addShadow(context, true, CGPoint(), 30.0, 0.0, UIColor(white: 0.0, alpha: 0.045), .normal) + addShadow(context, true, CGPoint(), 20.0, 0.0, UIColor(white: 0.0, alpha: 0.01), .normal) + + var a: CGFloat = 0.0 + var b: CGFloat = 0.0 + var s: CGFloat = 0.0 + fillColor.getHue(nil, saturation: &s, brightness: &b, alpha: &a) + + context.setFillColor(fillColor.cgColor) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii)) + context.fillPath() + + let lineWidth: CGFloat = (isDark ? 0.8 : 0.8) * borderWidthFactor + let strokeColor: UIColor + let blendMode: CGBlendMode + let baseAlpha: CGFloat = isDark ? 0.3 : 0.6 + + if s == 0.0 && abs(a - 0.7) < 0.1 && !isDark { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: baseAlpha) + } else if s <= 0.3 && !isDark { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: 0.7 * baseAlpha) + } else if b >= 0.2 { + let maxAlpha: CGFloat = isDark ? 0.7 : 0.8 + blendMode = .overlay + strokeColor = UIColor(white: 1.0, alpha: max(0.5, min(1.0, maxAlpha * s)) * baseAlpha) + } else { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: 0.5 * baseAlpha) + } + + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii)) + context.clip() + context.setBlendMode(blendMode) + context.setLineWidth(lineWidth) + context.setStrokeColor(strokeColor.cgColor) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect.insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadii: cornerRadii.insetBy(lineWidth * 0.5))) + context.strokePath() + context.resetClip() + context.setBlendMode(.normal) + }.stretchableImage(withLeftCapWidth: metrics.leftCapWidth, topCapHeight: metrics.topCapHeight) + } + static func generateForegroundImage(size: CGSize, isDark: Bool, fillColor: UIColor) -> UIImage { var size = size if size == .zero { @@ -1161,7 +1477,7 @@ public extension GlassBackgroundView { public final class GlassBackgroundComponent: Component { private let size: CGSize - private let cornerRadius: CGFloat + private let shape: GlassBackgroundView.Shape private let isDark: Bool private let tintColor: GlassBackgroundView.TintColor private let isInteractive: Bool @@ -1176,7 +1492,23 @@ public final class GlassBackgroundComponent: Component { isVisible: Bool = true ) { self.size = size - self.cornerRadius = cornerRadius + self.shape = .roundedRect(cornerRadius: cornerRadius) + self.isDark = isDark + self.tintColor = tintColor + self.isInteractive = isInteractive + self.isVisible = isVisible + } + + public init( + size: CGSize, + cornerRadii: GlassBackgroundView.CornerRadii, + isDark: Bool, + tintColor: GlassBackgroundView.TintColor, + isInteractive: Bool = false, + isVisible: Bool = true + ) { + self.size = size + self.shape = .customRoundedRect(cornerRadii: cornerRadii) self.isDark = isDark self.tintColor = tintColor self.isInteractive = isInteractive @@ -1187,7 +1519,7 @@ public final class GlassBackgroundComponent: Component { if lhs.size != rhs.size { return false } - if lhs.cornerRadius != rhs.cornerRadius { + if lhs.shape != rhs.shape { return false } if lhs.isDark != rhs.isDark { @@ -1207,7 +1539,7 @@ public final class GlassBackgroundComponent: Component { public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) + self.update(size: component.size, shape: component.shape, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) return component.size } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift index fd7a918447..7b5261d17c 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift @@ -88,17 +88,18 @@ final class LegacyGlassView: UIView { private struct Params: Equatable { let size: CGSize - let cornerRadius: CGFloat + let shape: GlassBackgroundView.Shape let style: Style - init(size: CGSize, cornerRadius: CGFloat, style: Style) { + init(size: CGSize, shape: GlassBackgroundView.Shape, style: Style) { self.size = size - self.cornerRadius = cornerRadius + self.shape = shape self.style = style } } private var params: Params? + private var maskLayer: CAShapeLayer? private let backdropLayer: CALayer? private let backdropLayerDelegate: BackdropLayerDelegate @@ -126,7 +127,11 @@ final class LegacyGlassView: UIView { } func update(size: CGSize, cornerRadius: CGFloat, style: Style, transition: ComponentTransition) { - let params = Params(size: size, cornerRadius: cornerRadius, style: style) + self.update(size: size, shape: .roundedRect(cornerRadius: cornerRadius), style: style, transition: transition) + } + + func update(size: CGSize, shape: GlassBackgroundView.Shape, style: Style, transition: ComponentTransition) { + let params = Params(size: size, shape: shape, style: style) let previousParams = self.params if self.params == params { return @@ -168,12 +173,31 @@ final class LegacyGlassView: UIView { } } - transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + switch shape { + case let .roundedRect(cornerRadius): + self.maskLayer = nil + self.layer.mask = nil + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + case let .customRoundedRect(cornerRadii): + transition.setCornerRadius(layer: self.layer, cornerRadius: 0.0) + + let maskLayer: CAShapeLayer + if let current = self.maskLayer { + maskLayer = current + } else { + maskLayer = CAShapeLayer() + maskLayer.fillColor = UIColor.black.cgColor + self.maskLayer = maskLayer + self.layer.mask = maskLayer + } + transition.setFrame(layer: maskLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setShapeLayerPath(layer: maskLayer, path: GlassBackgroundView.generateRoundedRectPath(size: size, cornerRadii: cornerRadii)) + } transition.setFrame(layer: backdropLayer, frame: CGRect(origin: CGPoint(), size: size)) if #available(iOS 17.0, *), DeviceMetrics.performance.isGraphicallyCapable { let size = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) - let cornerRadius = min(min(size.width, size.height) * 0.5, cornerRadius) + let cornerRadius = min(min(size.width, size.height) * 0.5, shape.maximumCornerRadius(for: size)) let displacementMagnitudePoints: CGFloat = 20.0 let displacementMagnitudeU = displacementMagnitudePoints / size.width let displacementMagnitudeV = displacementMagnitudePoints / size.height diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift index 3efa665988..9d60cd341d 100644 --- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift +++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift @@ -216,6 +216,7 @@ public final class GlassBarButtonComponent: Component { } } componentTransition.setFrame(view: view, frame: componentFrame) + componentTransition.setAlpha(view: view, alpha: component.isVisible ? 1.0 : 0.0) } let effectiveState: DisplayState = component.state ?? .glass diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index ce0204d62b..531138fe3c 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -15,7 +15,7 @@ public final class GlassControlGroupComponent: Component { case icon(String) case text(String) case animation(String) - case customIcon(id: AnyHashable, component: AnyComponent) + case customIcon(id: AnyHashable, component: AnyComponent, insets: UIEdgeInsets) enum Id: Hashable { case icon(String) @@ -32,7 +32,7 @@ public final class GlassControlGroupComponent: Component { return .text(text) case let .animation(animation): return .animation(animation) - case let .customIcon(id, _): + case let .customIcon(id, _, _): return .customIcon(id) } } @@ -216,8 +216,10 @@ public final class GlassControlGroupComponent: Component { size: CGSize(width: 32.0, height: 32.0), playOnce: playOnce )) - case let .customIcon(_, customIcon): + case let .customIcon(_, customIcon, insets): content = customIcon + itemInsets.left = insets.left + itemInsets.right = insets.right } var minItemWidth: CGFloat = availableSize.height @@ -285,8 +287,9 @@ public final class GlassControlGroupComponent: Component { let size = CGSize(width: contentsWidth, height: availableSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - isInteractive = true - self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition) + + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: true, transition: transition) + self.backgroundView.isUserInteractionEnabled = isInteractive return size } diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 64f26b5236..613a72b300 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil, 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) @@ -78,9 +78,19 @@ public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, ch } let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } + paintStickersContext.photoToolbarView = photoToolbarView controller.stickersContext = paintStickersContext controller.isImportant = true diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index d49f849c2b..c517dcedb9 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -804,7 +804,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { }))) let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - self.present(contextController) + self.presentInGlobalOverlay(contextController) } private func dismissAllTooltips() { @@ -859,7 +859,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } ) self.tooltipController = tooltipController - self.present(tooltipController) + self.presentInGlobalOverlay(tooltipController) } private func presentCaptionPositionTooltip(sourceView: UIView) { @@ -900,7 +900,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } ) self.tooltipController = tooltipController - self.present(tooltipController) + self.presentInGlobalOverlay(tooltipController) let _ = ApplicationSpecificNotice.incrementCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager).start() }) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 7736448cde..f16b416515 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -519,6 +519,8 @@ public final class ListActionItemComponent: Component { case .custom: contentLeftInset += 46.0 } + } else { + contentLeftInset += component.contentInsets.left } let titleSize = self.title.update( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 229c8257ae..f99112df82 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -44,6 +44,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState", "//submodules/DeviceAccess", "//submodules/LocationUI", + "//submodules/Weather", "//submodules/TelegramUI/Components/AudioWaveformComponent", "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/VolumeSliderContextItem", @@ -63,7 +64,6 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/WebsiteType", "//submodules/UrlEscaping", - "//submodules/DeviceLocationManager", "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/MediaAssetsContext", "//submodules/CheckNode", @@ -71,6 +71,7 @@ swift_library( "//submodules/TelegramUI/Components/AttachmentFileController", "//submodules/SaveToCameraRoll", "//submodules/TelegramUI/Components/ContextControllerImpl", + "//submodules/TelegramUI/Components/SliderComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift index 0b23bc037d..8d75439a5b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift @@ -2,11 +2,10 @@ import Foundation import UIKit import Display import ComponentFlow -import SwiftSignalKit -import LegacyComponents import MediaEditor +import SliderComponent -final class AdjustmentSliderComponent: Component { +private final class AdjustmentSliderRowComponent: Component { typealias EnvironmentType = Empty let title: String @@ -44,7 +43,7 @@ final class AdjustmentSliderComponent: Component { self.isTrackingUpdated = isTrackingUpdated } - static func ==(lhs: AdjustmentSliderComponent, rhs: AdjustmentSliderComponent) -> Bool { + static func ==(lhs: AdjustmentSliderRowComponent, rhs: AdjustmentSliderRowComponent) -> Bool { if lhs.title != rhs.title { return false } @@ -72,12 +71,12 @@ final class AdjustmentSliderComponent: Component { return true } - final class View: UIView, UITextFieldDelegate { + final class View: UIView { private let title = ComponentView() private let value = ComponentView() - private var sliderView: TGPhotoEditorSliderView? + private let slider = ComponentView() - private var component: AdjustmentSliderComponent? + private var component: AdjustmentSliderRowComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { @@ -88,22 +87,15 @@ final class AdjustmentSliderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: AdjustmentSliderRowComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state var internalIsTrackingUpdated: ((Bool) -> Void)? if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in + isTrackingUpdated(isTracking) if let self { - if isTracking { - self.sliderView?.bordered = true - } else { - Queue.mainQueue().after(0.1) { - self.sliderView?.bordered = false - } - } - isTrackingUpdated(isTracking) let transition: ComponentTransition if isTracking { transition = .immediate @@ -119,52 +111,27 @@ final class AdjustmentSliderComponent: Component { } } } - - let sliderView: TGPhotoEditorSliderView - if let current = self.sliderView { - sliderView = current - sliderView.value = CGFloat(component.value) - } else { - sliderView = TGPhotoEditorSliderView() - sliderView.backgroundColor = .clear - sliderView.startColor = UIColor(rgb: 0xffffff) - sliderView.enablePanHandling = true - sliderView.trackCornerRadius = 1.0 - sliderView.lineSize = 2.0 - sliderView.minimumValue = CGFloat(component.minValue) - sliderView.maximumValue = CGFloat(component.maxValue) - sliderView.startValue = CGFloat(component.startValue) - sliderView.value = CGFloat(component.value) - sliderView.disablesInteractiveTransitionGestureRecognizer = true - sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) - sliderView.layer.allowsGroupOpacity = true - self.sliderView = sliderView - self.addSubview(sliderView) - } - sliderView.interactionBegan = { - internalIsTrackingUpdated?(true) - } - sliderView.interactionEnded = { - internalIsTrackingUpdated?(false) - } - if component.isEnabled { - sliderView.alpha = 1.3 - sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff) - sliderView.isUserInteractionEnabled = true + var hasValue = false + let valueText: String + if component.displayValue { + if component.value > 0.005 { + valueText = String(format: "+%.2f", component.value) + hasValue = true + } else if component.value < -0.005 { + valueText = String(format: "%.2f", component.value) + hasValue = true + } else { + valueText = "" + } } else { - sliderView.trackColor = UIColor(rgb: 0xffffff) - sliderView.alpha = 0.3 - sliderView.isUserInteractionEnabled = false + valueText = "" } - transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0))) - sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) - let titleSize = self.title.update( transition: .immediate, component: AnyComponent( - Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080)) + Text(text: component.title, font: Font.bold(13.0), color: hasValue ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x808080)) ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -173,26 +140,13 @@ final class AdjustmentSliderComponent: Component { if titleView.superview == nil { self.addSubview(titleView) } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize)) - } - - let valueText: String - if component.displayValue { - if component.value > 0.005 { - valueText = String(format: "+%.2f", component.value) - } else if component.value < -0.005 { - valueText = String(format: "%.2f", component.value) - } else { - valueText = "" - } - } else { - valueText = "" + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 30.0, y: 0.0), size: titleSize)) } let valueSize = self.value.update( transition: .immediate, component: AnyComponent( - Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xffd300)) + Text(text: valueText, font: Font.with(size: 13.0, weight: .medium, traits: .monospacedNumbers), color: UIColor(rgb: 0xffd300)) ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -201,17 +155,44 @@ final class AdjustmentSliderComponent: Component { if valueView.superview == nil { self.addSubview(valueView) } - transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize)) + transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 30.0 - valueSize.width, y: 0.0), size: valueSize)) } - return CGSize(width: availableSize.width, height: 52.0) - } - - @objc private func sliderValueChanged() { - guard let component = self.component, let sliderView = self.sliderView else { - return + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent( + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(component.value), + range: CGFloat(component.minValue) ... CGFloat(component.maxValue), + startValue: CGFloat(component.startValue), + valueUpdated: { [weak self] value in + guard let self, let component = self.component else { + return + } + component.valueUpdated(Float(value)) + } + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: component.isEnabled ? (component.trackColor ?? UIColor(rgb: 0xffffff)) : UIColor(rgb: 0xffffff), + isEnabled: component.isEnabled, + trackHeight: 6.0, + displaysBorderOnTracking: component.isTrackingUpdated != nil, + isTrackingUpdated: internalIsTrackingUpdated + ) + ), + environment: {}, + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) + ) + if let sliderView = self.slider.view { + if sliderView.superview == nil { + self.addSubview(sliderView) + } + transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 28.0, y: 7.0), size: sliderSize)) } - component.valueUpdated(Float(sliderView.value)) + + return CGSize(width: availableSize.width, height: 52.0) } } @@ -325,14 +306,14 @@ final class AdjustmentsComponent: Component { let size = componentView.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( + AdjustmentSliderRowComponent( title: tool.title, value: value, minValue: tool.minValue, maxValue: tool.maxValue, startValue: tool.startValue, isEnabled: true, - trackColor: nil, + trackColor: UIColor(rgb: 0xffd300), displayValue: true, valueUpdated: { value in var updatedValue = value @@ -363,7 +344,7 @@ final class AdjustmentsComponent: Component { } transition.setFrame(view: view, frame: CGRect(origin: origin, size: size)) } - origin = origin.offsetBy(dx: 0.0, dy: size.height) + origin = origin.offsetBy(dx: 0.0, dy: size.height + 8.0) } let size = CGSize(width: availableSize.width, height: 180.0) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift index 3cf59203b7..47019ceb17 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift @@ -2,9 +2,9 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents import MediaEditor import TelegramPresentationData +import SliderComponent private final class BlurModeComponent: Component { typealias EnvironmentType = Empty @@ -73,7 +73,7 @@ private final class BlurModeComponent: Component { component: AnyComponent( Text( text: component.title, - font: Font.regular(14.0), + font: Font.bold(13.0), color: component.isSelected ? UIColor(rgb: 0xffd300) : UIColor(rgb: 0x808080) ) ), @@ -198,7 +198,7 @@ final class BlurComponent: Component { component: AnyComponent( Text( text: component.strings.Story_Editor_Blur_Title, - font: Font.regular(14.0), + font: Font.bold(13.0), color: UIColor(rgb: 0x808080) ) ), @@ -331,27 +331,30 @@ final class BlurComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( - title: "", - value: state.value.intensity, - minValue: 0.0, - maxValue: 1.0, - startValue: 0.0, - isEnabled: state.value.mode != .off, - trackColor: nil, - displayValue: false, - valueUpdated: { [weak state] value in - if let state { - valueUpdated(state.value.withUpdatedIntensity(value)) + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(state.value.intensity), + range: 0.0 ... 1.0, + startValue: 0.0, + valueUpdated: { [weak state] value in + if let state { + valueUpdated(state.value.withUpdatedIntensity(Float(value))) + } } - }, + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: state.value.mode != .off ? UIColor(rgb: 0xffd300) : UIColor(rgb: 0xffffff), + isEnabled: state.value.mode != .off, + trackHeight: 6.0, + displaysBorderOnTracking: true, isTrackingUpdated: { isTracking in isTrackingUpdated(isTracking) } ) ), environment: {}, - containerSize: availableSize + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) ) var buttons = [self.offButton, self.radialButton, self.linearButton] @@ -375,7 +378,8 @@ final class BlurComponent: Component { } let verticalSpacing: CGFloat = -5.0 - let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + offButtonSize.height + verticalSpacing), size: sliderSize) + let sliderRowHeight: CGFloat = 52.0 + let sliderFrame = CGRect(origin: CGPoint(x: 28.0, y: topInset + offButtonSize.height + verticalSpacing + 7.0), size: sliderSize) if let view = self.slider.view { if view.superview == nil { self.addSubview(view) @@ -383,7 +387,7 @@ final class BlurComponent: Component { transition.setFrame(view: view, frame: sliderFrame) } - return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderSize.height + 6.0) + return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderRowHeight + 6.0) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift index 924459dc45..7311d63ff8 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift @@ -153,7 +153,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_All, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .all ? .white : UIColor(rgb: 0x808080) ) ), @@ -181,7 +181,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Red, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .red ? .white : UIColor(rgb: 0x808080) ) ), @@ -209,7 +209,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Green, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .green ? .white : UIColor(rgb: 0x808080) ) ), @@ -237,7 +237,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Blue, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .blue ? .white : UIColor(rgb: 0x808080) ) ), @@ -536,7 +536,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.blacks), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -563,7 +563,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.shadows), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -590,7 +590,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.midtones), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -617,7 +617,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.highlights), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -644,7 +644,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.whites), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5a77bf9af7..9e8d198831 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -52,6 +52,9 @@ import SaveProgressScreen import TelegramNotices import AttachmentFileController import SaveToCameraRoll +import GlassBarButtonComponent +import GlassBackgroundComponent +import Weather private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -198,7 +201,7 @@ final class MediaEditorScreenComponent: Component { case .sticker: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! case .tools: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Adjustments"), color: .white)! case .rotate: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Rotate"), color: .white)! case .flip: @@ -300,6 +303,8 @@ final class MediaEditorScreenComponent: Component { public final class View: UIView { private let cancelButton = ComponentView() private let doneButton = ComponentView() + + private let buttonsBackgroundView = GlassBackgroundView() private let drawButton = ComponentView() private let textButton = ComponentView() private let stickerButton = ComponentView() @@ -675,9 +680,6 @@ final class MediaEditorScreenComponent: Component { } func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { - if let view = self.cancelButton.view { - view.alpha = 0.0 - } let buttons = [ self.drawButton, self.textButton, @@ -686,12 +688,12 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - if !inPlace { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } + if let view = self.cancelButton.view { + transition.setScale(view: view, scale: 0.1) + } if let view = self.doneButton.view { transition.setScale(view: view, scale: 0.1) } @@ -704,12 +706,6 @@ final class MediaEditorScreenComponent: Component { } func animateInFromTool(inPlace: Bool, transition: ComponentTransition) { - if let view = self.cancelButton.view { - view.alpha = 1.0 - } - if let buttonView = self.cancelButton.view as? Button.View, let view = buttonView.content as? LottieAnimationComponent.View { - view.playOnce() - } let buttons = [ self.drawButton, self.textButton, @@ -718,12 +714,12 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - if !inPlace { - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } + if let view = self.cancelButton.view { + transition.setScale(view: view, scale: 1.0) + } if let view = self.doneButton.view { transition.setScale(view: view, scale: 1.0) } @@ -821,10 +817,17 @@ final class MediaEditorScreenComponent: Component { buttonSideInset = 30.0 } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) - buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { topInset = 0.0 - controlsBottomInset = -50.0 + controlsBottomInset = -62.0 + buttonSideInset = 9.0 + } else { + if availableSize.width > 320.0 { + buttonSideInset = 16.0 + } else { + buttonSideInset = 9.0 + } } } var previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: previewSize) @@ -835,30 +838,36 @@ final class MediaEditorScreenComponent: Component { let bottomButtonsAlpha: CGFloat = isRecordingAdditionalVideo ? 0.3 : 1.0 let buttonsAreHidden = component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities + if self.buttonsBackgroundView.superview == nil { + self.addSubview(self.buttonsBackgroundView) + } + let cancelButtonSize = self.cancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .still(position: .end), - range: (0.5, 1.0) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { [weak controller] in - guard let controller else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white) + ) + ), + action: { [weak controller] _ in + guard let controller else { + return + } + guard !controller.node.recording.isActive else { + return + } + controller.maybePresentDiscardAlert() } - guard !controller.node.recording.isActive else { - return - } - controller.maybePresentDiscardAlert() - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -872,35 +881,83 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) - transition.setAlpha(view: cancelButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - var doneButtonTitle: String? - var doneButtonIcon: UIImage? + let doneButtonFont = Font.with(size: 16.0, design: .round, weight: .semibold) + var doneButtonItems: [AnyComponentWithIdentity] = [] + var doneButtonExplicitSize: CGSize? switch controller.mode { case .storyEditor: - doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased() - doneButtonIcon = UIImage(bundleImageName: "Media Editor/Next")! + if availableSize.width > 320.0 { + doneButtonItems = [ + AnyComponentWithIdentity( + id: "label", + component: AnyComponent( + Text(text: isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased(), font: doneButtonFont, color: .white) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Media Editor/Next", tintColor: .white) + ) + ) + ] + } else { + doneButtonExplicitSize = CGSize(width: 44.0, height: 44.0) + doneButtonItems = [ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white, flipHorizontally: true) + ) + ) + ] + } case .stickerEditor, .avatarEditor, .coverEditor: - doneButtonTitle = nil - doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)! + doneButtonExplicitSize = CGSize(width: 44.0, height: 44.0) + doneButtonItems = [ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) + ) + ] case .botPreview: - doneButtonTitle = environment.strings.Story_Editor_Add.uppercased() - doneButtonIcon = nil + doneButtonItems = [ + AnyComponentWithIdentity( + id: "label", + component: AnyComponent( + Text(text: environment.strings.Story_Editor_Add.uppercased(), font: doneButtonFont, color: .white) + ) + ) + ] } let doneButtonSize = self.doneButton.update( transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(DoneButtonContentComponent( + component: AnyComponent( + GlassBarButtonComponent( + size: doneButtonExplicitSize, backgroundColor: UIColor(rgb: 0x0088ff), - icon: doneButtonIcon, - title: doneButtonTitle)), - effectAlignment: .center, - action: { [weak controller] in - controller?.node.requestCompletion() - } - )), + isDark: true, + state: .tintedGlass, + isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + HStack( + doneButtonItems, + spacing: 5.0 + ) + ) + ), + action: { [weak controller] _ in + controller?.node.requestCompletion() + } + ) + ), environment: {}, containerSize: CGSize(width: availableSize.width, height: 44.0) ) @@ -914,19 +971,21 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: doneButtonView, position: doneButtonFrame.center) transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) - transition.setAlpha(view: doneButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - - let buttonsAvailableWidth: CGFloat - let buttonsLeftOffset: CGFloat - if isTablet { - buttonsAvailableWidth = previewSize.width + 180.0 - buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) - } else { - buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0) - buttonsLeftOffset = floorToScreenPixels(buttonSideInset + cancelButtonSize.width * 0.66) + + let buttonSize = CGSize(width: 44.0, height: 44.0) + let buttonSpacing: CGFloat = 10.0 + var buttonCount = 4 + if let subject = controller.node.subject, case .empty = subject { + buttonCount = 3 } + let buttonsTotalWidth: CGFloat = buttonSize.width * CGFloat(buttonCount) + buttonSpacing * CGFloat(buttonCount - 1) + let buttonsBackgroundFrame = CGRect(x: cancelButtonFrame.maxX + floorToScreenPixels(((doneButtonFrame.minX - cancelButtonFrame.maxX) - buttonsTotalWidth) / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset, width: buttonsTotalWidth, height: buttonSize.height) + transition.setFrame(view: self.buttonsBackgroundView, frame: buttonsBackgroundFrame) + self.buttonsBackgroundView.update(size: buttonsBackgroundFrame.size, cornerRadius: buttonsBackgroundFrame.size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, transition: transition) + + var buttonOriginX: CGFloat = 0.0 let drawButtonSize = self.drawButton.update( transition: transition, component: AnyComponent(ContextReferenceButtonComponent( @@ -935,7 +994,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: drawButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] _, _ in guard let controller else { return @@ -947,13 +1006,14 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var drawButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 - drawButtonSize.width / 2.0 - 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: drawButtonSize - ) - + ) + buttonOriginX += drawButtonSize.width + buttonSpacing + let textButtonSize = self.textButton.update( transition: transition, component: AnyComponent(ContextReferenceButtonComponent( @@ -962,7 +1022,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] _, _ in guard let controller else { return @@ -974,12 +1034,13 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var textButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 2.0 - textButtonSize.width / 2.0 - 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: textButtonSize ) + buttonOriginX += textButtonSize.width + buttonSpacing let stickerButtonSize = self.stickerButton.update( transition: transition, @@ -989,7 +1050,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: stickerButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] view, gesture in guard let controller else { return @@ -1005,12 +1066,13 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var stickerButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 3.0 - stickerButtonSize.width / 2.0 + 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: stickerButtonSize ) + buttonOriginX += drawButtonSize.width + buttonSpacing let rotateButtonSize = self.rotateButton.update( transition: transition, @@ -1020,7 +1082,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller, weak mediaEditor] _, _ in guard let controller, let mediaEditor else { return @@ -1037,10 +1099,10 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let rotateButtonFrame = CGRect( - origin: CGPoint(x: drawButtonFrame.origin.x, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: drawButtonFrame.origin.x, y: 0.0), size: rotateButtonSize ) @@ -1052,7 +1114,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller, weak mediaEditor] _, _ in guard let controller, let mediaEditor else { return @@ -1069,10 +1131,10 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let flipButtonFrame = CGRect( - origin: CGPoint(x: textButtonFrame.origin.x, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: textButtonFrame.origin.x, y: 0.0), size: flipButtonSize ) @@ -1089,7 +1151,7 @@ final class MediaEditorScreenComponent: Component { if let rotateButtonView = self.rotateButton.view { if rotateButtonView.superview == nil { - self.addSubview(rotateButtonView) + self.buttonsBackgroundView.contentView.addSubview(rotateButtonView) } transition.setPosition(view: rotateButtonView, position: rotateButtonFrame.center) transition.setBounds(view: rotateButtonView, bounds: CGRect(origin: .zero, size: rotateButtonFrame.size)) @@ -1100,7 +1162,7 @@ final class MediaEditorScreenComponent: Component { if let flipButtonView = self.flipButton.view { if flipButtonView.superview == nil { - self.addSubview(flipButtonView) + self.buttonsBackgroundView.contentView.addSubview(flipButtonView) } transition.setPosition(view: flipButtonView, position: flipButtonFrame.center) transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size)) @@ -1117,7 +1179,7 @@ final class MediaEditorScreenComponent: Component { if let drawButtonView = self.drawButton.view { if drawButtonView.superview == nil { - self.addSubview(drawButtonView) + self.buttonsBackgroundView.contentView.addSubview(drawButtonView) } transition.setPosition(view: drawButtonView, position: drawButtonFrame.center) transition.setBounds(view: drawButtonView, bounds: CGRect(origin: .zero, size: drawButtonFrame.size)) @@ -1128,7 +1190,7 @@ final class MediaEditorScreenComponent: Component { if !isAvatarEditor && !isCoverEditor, let textButtonView = self.textButton.view { if textButtonView.superview == nil { - self.addSubview(textButtonView) + self.buttonsBackgroundView.contentView.addSubview(textButtonView) } transition.setPosition(view: textButtonView, position: textButtonFrame.center) transition.setBounds(view: textButtonView, bounds: CGRect(origin: .zero, size: textButtonFrame.size)) @@ -1139,7 +1201,7 @@ final class MediaEditorScreenComponent: Component { if !isAvatarEditor && !isCoverEditor, let stickerButtonView = self.stickerButton.view { if stickerButtonView.superview == nil { - self.addSubview(stickerButtonView) + self.buttonsBackgroundView.contentView.addSubview(stickerButtonView) } transition.setPosition(view: stickerButtonView, position: stickerButtonFrame.center) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: .zero, size: stickerButtonFrame.size)) @@ -1155,12 +1217,14 @@ final class MediaEditorScreenComponent: Component { } else { let toolsButtonSize = self.toolsButton.update( transition: transition, - component: AnyComponent(Button( + component: AnyComponent(ContextReferenceButtonComponent( content: AnyComponent(Image( image: state.image(.tools), size: CGSize(width: 30.0, height: 30.0) )), - action: { [weak controller] in + tag: textButtonTag, + minSize: buttonSize, + action: { [weak controller] _, _ in guard let controller else { return } @@ -1171,15 +1235,15 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 4.0 - toolsButtonSize.width / 2.0 + 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { if toolsButtonView.superview == nil { - self.addSubview(toolsButtonView) + self.buttonsBackgroundView.contentView.addSubview(toolsButtonView) } transition.setPosition(view: toolsButtonView, position: toolsButtonFrame.center) transition.setBounds(view: toolsButtonView, bounds: CGRect(origin: .zero, size: toolsButtonFrame.size)) @@ -1643,31 +1707,37 @@ final class MediaEditorScreenComponent: Component { let saveButtonSize = self.saveButton.update( transition: transition, - component: AnyComponent(CameraButton( - content: saveContentComponent, - action: { [weak self, weak controller] in - guard let self, let controller else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + isVisible: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && topButtonsAlpha > 0.0, + component: saveContentComponent, + action: { [weak self, weak controller] _ in + guard let self, let controller else { + return + } + guard !controller.node.recording.isActive else { + return + } + if let view = self.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { + view.playOnce() + } + controller.requestSave() } - guard !controller.node.recording.isActive else { - return - } - if let view = self.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { - view.playOnce() - } - controller.requestSave() - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let saveButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + origin: CGPoint(x: availableSize.width - 16.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: saveButtonSize ) if let saveButtonView = self.saveButton.view { if saveButtonView.superview == nil { - setupButtonShadow(saveButtonView) self.addSubview(saveButtonView) } @@ -1677,7 +1747,7 @@ final class MediaEditorScreenComponent: Component { buttonTransition.setPosition(view: saveButtonView, position: saveButtonFrame.center) buttonTransition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) + transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && saveButtonAlpha > 0.0 ? saveButtonAlpha : 1.0) } var topButtonOffsetX: CGFloat = 0.0 @@ -9230,3 +9300,31 @@ private struct MediaEditorConfiguration { } } } + +private func getWeather(context: AccountContext, load: Bool) -> Signal { + return Weather.getWeatherData(context: context, load: load) + |> map { state -> StickerPickerScreen.Weather in + switch state { + case .none: + return .none + case .notDetermined: + return .notDetermined + case .notAllowed: + return .notAllowed + case .notPreloaded: + return .notPreloaded + case .fetching: + return .fetching + case let .loaded(weather): + if let match = context.animatedEmojiStickersValue[weather.emoji]?.first { + return .loaded(StickerPickerScreen.Weather.LoadedWeather( + emoji: weather.emoji, + emojiFile: match.file._parse(), + temperature: weather.temperature + )) + } else { + return .none + } + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index ec53f4d4eb..acfb68e235 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -13,8 +13,10 @@ import MultilineTextComponent import DrawingUI import MediaEditor import Photos -import LottieAnimationComponent import MessageInputPanelComponent +import BundleIconComponent +import GlassBarButtonComponent +import GlassBackgroundComponent private enum MediaToolsSection: Equatable { case adjustments @@ -54,7 +56,6 @@ private final class ToolIconComponent: Component { } final class View: UIView { - private let selection = SimpleShapeLayer() private let icon = ComponentView() private var component: ToolIconComponent? @@ -62,10 +63,6 @@ private final class ToolIconComponent: Component { override init(frame: CGRect) { super.init(frame: frame) - - self.selection.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), cornerRadius: 10.0).cgPath - self.selection.fillColor = UIColor(rgb: 0xd1d1d1).cgColor - self.layer.addSublayer(self.selection) } required init?(coder: NSCoder) { @@ -105,8 +102,6 @@ private final class ToolIconComponent: Component { transition.setFrame(view: view, frame: iconFrame) } - self.selection.isHidden = !component.isSelected - return size } } @@ -163,7 +158,6 @@ private final class MediaToolsScreenComponent: Component { case tint case blur case curves - case done } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -173,15 +167,13 @@ private final class MediaToolsScreenComponent: Component { var image: UIImage switch key { case .adjustments: - image = UIImage(bundleImageName: "Media Editor/Tools")! + image = UIImage(bundleImageName: "Media Editor/Adjustments")! case .tint: image = UIImage(bundleImageName: "Media Editor/Tint")! case .blur: image = UIImage(bundleImageName: "Media Editor/Blur")! case .curves: image = UIImage(bundleImageName: "Media Editor/Curves")! - case .done: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! } cachedImages[key] = image return image @@ -220,7 +212,8 @@ private final class MediaToolsScreenComponent: Component { public final class View: UIView { private let buttonsContainerView = UIView() - private let buttonsBackgroundView = UIView() + private let buttonsBackgroundView = GlassBackgroundView() + private let selectedToolBackgroundLayer = SimpleShapeLayer() private let cancelButton = ComponentView() private let adjustmentsButton = ComponentView() private let tintButton = ComponentView() @@ -248,7 +241,7 @@ private final class MediaToolsScreenComponent: Component { self.optionsBackgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.9) super.init(frame: frame) - + self.backgroundColor = .clear self.addSubview(self.previewContainerView) @@ -256,6 +249,10 @@ private final class MediaToolsScreenComponent: Component { self.previewContainerView.addSubview(self.optionsContainerView) self.optionsContainerView.addSubview(self.optionsBackgroundView) self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.selectedToolBackgroundLayer.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), cornerRadius: 16.5).cgPath + self.selectedToolBackgroundLayer.fillColor = UIColor(rgb: 0xd1d1d1).cgColor + self.buttonsBackgroundView.contentView.layer.insertSublayer(self.selectedToolBackgroundLayer, at: 0) } required init?(coder: NSCoder) { @@ -269,20 +266,17 @@ private final class MediaToolsScreenComponent: Component { self.blurButton, self.curvesButton ] - - var delay: Double = 0.0 for button in buttons { if let view = button.view { - view.alpha = 0.0 - Queue.mainQueue().after(delay, { - view.alpha = 1.0 - view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - }) - delay += 0.03 + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) } } + + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } if let view = self.doneButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -297,9 +291,7 @@ private final class MediaToolsScreenComponent: Component { private var animatingOut = false func animateOutToEditor(completion: @escaping () -> Void) { self.animatingOut = true - - self.cancelButton.view?.isHidden = true - + let buttons = [ self.adjustmentsButton, self.tintButton, @@ -309,17 +301,21 @@ private final class MediaToolsScreenComponent: Component { for button in buttons { if let view = button.view { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { _ in + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + if let view = self.doneButton.view { view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) } if let view = self.toolScreen?.view { @@ -364,12 +360,17 @@ private final class MediaToolsScreenComponent: Component { buttonSideInset = 30.0 } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) - buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { topInset = 0.0 controlsBottomInset = -75.0 + buttonSideInset = 9.0 } else { - self.buttonsBackgroundView.backgroundColor = .clear + if availableSize.width > 320.0 { + buttonSideInset = 16.0 + } else { + buttonSideInset = 9.0 + } } } @@ -378,25 +379,26 @@ private final class MediaToolsScreenComponent: Component { let cancelButtonSize = self.cancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .animating(loop: false), - range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { - guard let controller = environment.controller() as? MediaToolsScreen else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white) + ) + ), + action: { _ in + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(reset: true, animated: true) } - controller.requestDismiss(reset: true, animated: true) - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -413,18 +415,26 @@ private final class MediaToolsScreenComponent: Component { let doneButtonSize = self.doneButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image( - image: state.image(.done), - size: CGSize(width: 33.0, height: 33.0) - )), - action: { - guard let controller = environment.controller() as? MediaToolsScreen else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: UIColor(rgb: 0x0088ff), + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) + ), + action: { _ in + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(reset: false, animated: true) } - controller.requestDismiss(reset: false, animated: true) - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -439,15 +449,19 @@ private final class MediaToolsScreenComponent: Component { transition.setFrame(view: doneButtonView, frame: doneButtonFrame) } - let buttonsAvailableWidth: CGFloat - let buttonsLeftOffset: CGFloat - if isTablet { - buttonsAvailableWidth = previewSize.width + 260.0 - buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) - } else { - buttonsAvailableWidth = availableSize.width - buttonsLeftOffset = 0.0 - } + let buttonSize = CGSize(width: 44.0, height: 44.0) + let buttonSpacing: CGFloat = 10.0 + let buttonsTotalWidth = buttonSize.width * 4.0 + buttonSpacing * 3.0 + let buttonsBackgroundFrame = CGRect( + x: cancelButtonFrame.maxX + floorToScreenPixels((doneButtonFrame.minX - cancelButtonFrame.maxX - buttonsTotalWidth) / 2.0), + y: buttonBottomInset, + width: buttonsTotalWidth, + height: buttonSize.height + ) + transition.setFrame(view: self.buttonsBackgroundView, frame: buttonsBackgroundFrame) + self.buttonsBackgroundView.update(size: buttonsBackgroundFrame.size, cornerRadius: buttonsBackgroundFrame.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + + var buttonOriginX: CGFloat = 0.0 let adjustmentsButtonSize = self.adjustmentsButton.update( transition: transition, @@ -460,17 +474,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.adjustments) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let adjustmentsButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: adjustmentsButtonSize ) + buttonOriginX += adjustmentsButtonSize.width + buttonSpacing if let adjustmentsButtonView = self.adjustmentsButton.view { if adjustmentsButtonView.superview == nil { - self.buttonsContainerView.addSubview(adjustmentsButtonView) + self.buttonsBackgroundView.contentView.addSubview(adjustmentsButtonView) } transition.setFrame(view: adjustmentsButtonView, frame: adjustmentsButtonFrame) } @@ -486,17 +501,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.tint) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let tintButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: tintButtonSize ) + buttonOriginX += tintButtonSize.width + buttonSpacing if let tintButtonView = self.tintButton.view { if tintButtonView.superview == nil { - self.buttonsContainerView.addSubview(tintButtonView) + self.buttonsBackgroundView.contentView.addSubview(tintButtonView) } transition.setFrame(view: tintButtonView, frame: tintButtonFrame) } @@ -512,17 +528,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.blur) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let blurButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: blurButtonSize ) + buttonOriginX += blurButtonSize.width + buttonSpacing if let blurButtonView = self.blurButton.view { if blurButtonView.superview == nil { - self.buttonsContainerView.addSubview(blurButtonView) + self.buttonsBackgroundView.contentView.addSubview(blurButtonView) } transition.setFrame(view: blurButtonView, frame: blurButtonFrame) } @@ -538,21 +555,42 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.curves) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let curvesButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: curvesButtonSize ) if let curvesButtonView = self.curvesButton.view { if curvesButtonView.superview == nil { - self.buttonsContainerView.addSubview(curvesButtonView) + self.buttonsBackgroundView.contentView.addSubview(curvesButtonView) } transition.setFrame(view: curvesButtonView, frame: curvesButtonFrame) } + let selectedButtonFrame: CGRect + switch component.section { + case .adjustments: + selectedButtonFrame = adjustmentsButtonFrame + case .tint: + selectedButtonFrame = tintButtonFrame + case .blur: + selectedButtonFrame = blurButtonFrame + case .curves: + selectedButtonFrame = curvesButtonFrame + } + let selectedToolBackgroundSize = CGSize(width: 33.0, height: 33.0) + let selectedToolBackgroundFrame = CGRect( + origin: CGPoint( + x: floorToScreenPixels(selectedButtonFrame.midX - selectedToolBackgroundSize.width / 2.0), + y: floorToScreenPixels(selectedButtonFrame.midY - selectedToolBackgroundSize.height / 2.0) + ), + size: selectedToolBackgroundSize + ) + transition.setFrame(layer: self.selectedToolBackgroundLayer, frame: selectedToolBackgroundFrame) + var sectionChanged = false if previousSection != component.section { sectionChanged = true @@ -938,7 +976,6 @@ private final class MediaToolsScreenComponent: Component { transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) - transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) return availableSize } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index 320a81ed35..ea4942a2e6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -110,6 +110,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift index 0f6a0afce0..89dc8bbf4b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift @@ -2,9 +2,9 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents import MediaEditor import TelegramPresentationData +import SliderComponent private final class TintColorComponent: Component { typealias EnvironmentType = Empty @@ -193,7 +193,7 @@ final class TintComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Tint_Shadows, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .shadows ? .white : UIColor(rgb: 0x808080) ) ), @@ -221,7 +221,7 @@ final class TintComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Tint_Highlights, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .highlights ? .white : UIColor(rgb: 0x808080) ) ), @@ -339,32 +339,35 @@ final class TintComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( - title: "", - value: state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity, - minValue: 0.0, - maxValue: 1.0, - startValue: 0.0, - isEnabled: currentColor != .clear, - trackColor: currentColor != .clear ? currentColor : .white, - displayValue: false, - valueUpdated: { [weak state] value in - if let state { - switch state.section { - case .shadows: - shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(value)) - case .highlights: - highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(value)) + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity), + range: 0.0 ... 1.0, + startValue: 0.0, + valueUpdated: { [weak state] value in + if let state { + switch state.section { + case .shadows: + shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(Float(value))) + case .highlights: + highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(Float(value))) + } } } - }, + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: currentColor != .clear ? currentColor : .white, + isEnabled: currentColor != .clear, + trackHeight: 6.0, + displaysBorderOnTracking: true, isTrackingUpdated: { isTracking in isTrackingUpdated(isTracking) } ) ), environment: {}, - containerSize: availableSize + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) ) let colorsVerticalSpacing: CGFloat = 9.0 @@ -387,7 +390,8 @@ final class TintComponent: Component { } let verticalSpacing: CGFloat = 3.0 - let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing), size: sliderSize) + let sliderRowHeight: CGFloat = 52.0 + let sliderFrame = CGRect(origin: CGPoint(x: 28.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing + 7.0), size: sliderSize) if let view = self.slider.view { if view.superview == nil { self.addSubview(view) @@ -395,7 +399,7 @@ final class TintComponent: Component { transition.setFrame(view: view, frame: sliderFrame) } - return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderSize.height) + return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderRowHeight) } } @@ -407,4 +411,3 @@ final class TintComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift index 8c2f4edb12..2099084833 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -351,6 +351,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 6a5074fdc5..5ad64b0bbb 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1188,13 +1188,18 @@ public final class MessageInputPanelComponent: Component { let previousPlaceholder = self.component?.placeholder - let defaultInsets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) + let defaultSideInset: CGFloat = component.style == .media ? 16.0 : 9.0 + let defaultInsets = UIEdgeInsets(top: 14.0, left: defaultSideInset, bottom: 6.0, right: 41.0) var insets = defaultInsets let layoutFromTop = component.attachmentButtonMode == .captionDown if let _ = component.attachmentAction { - insets.left = 41.0 + if case .media = component.style { + insets.left = 54.0 + } else { + insets.left = 41.0 + } } if let _ = component.setMediaRecordingActive { insets.right = 41.0 @@ -1202,7 +1207,9 @@ public final class MessageInputPanelComponent: Component { let textFieldSideInset: CGFloat switch component.style { - case .media, .videoChat, .gift: + case .media: + textFieldSideInset = 16.0 + case .videoChat, .gift: textFieldSideInset = 8.0 default: textFieldSideInset = 9.0 @@ -1326,7 +1333,7 @@ public final class MessageInputPanelComponent: Component { let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize: CGSize - var placeholderColor = UIColor(rgb: 0xffffff, alpha: 0.4) + var placeholderColor = UIColor(rgb: 0xffffff, alpha: 0.35) if case .gift = component.style { placeholderColor = component.theme.chat.inputPanel.inputPlaceholderColor } @@ -1483,11 +1490,9 @@ public final class MessageInputPanelComponent: Component { } var fieldBackgroundFrame: CGRect - if hasMediaRecording { + if hasMediaRecording || [.videoChat, .gift].contains(component.style) { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) - } else if [.videoChat, .gift].contains(component.style) { - fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) - } else if isEditing || component.style == .editor || component.style == .media { + } else if isEditing || [.editor, .media].contains(component.style) { fieldBackgroundFrame = fieldFrame } else { if component.forwardAction != nil && component.likeAction != nil { @@ -1502,10 +1507,8 @@ public final class MessageInputPanelComponent: Component { let rawFieldBackgroundFrame = fieldBackgroundFrame fieldBackgroundFrame.size.height += headerHeight - //transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) - switch component.style { - case .gift: + case .media, .editor, .gift, .videoChat: if self.fieldGlassBackgroundView == nil { let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) @@ -1515,20 +1518,14 @@ public final class MessageInputPanelComponent: Component { self.fieldBackgroundTint.isHidden = true } if let fieldGlassBackgroundView = self.fieldGlassBackgroundView { - fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) - transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame) - } - case .videoChat: - if self.fieldGlassBackgroundView == nil { - let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) - self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) - self.fieldGlassBackgroundView = fieldGlassBackgroundView - - self.fieldBackgroundView.isHidden = true - self.fieldBackgroundTint.isHidden = true - } - if let fieldGlassBackgroundView = self.fieldGlassBackgroundView { - fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: .init(kind: .custom(style: .default, color: UIColor(rgb: 0x25272e, alpha: 0.72))), transition: transition) + let tintColor: GlassBackgroundView.TintColor + switch component.style { + case .videoChat: + tintColor = .init(kind: .custom(style: .default, color: UIColor(rgb: 0x25272e, alpha: 0.72))) + default: + tintColor = .init(kind: .panel) + } + fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, transition: transition) transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame) } default: @@ -1540,7 +1537,6 @@ public final class MessageInputPanelComponent: Component { transition.setFrame(view: self.fieldBackgroundTint, frame: fieldBackgroundFrame) transition.setFrame(view: self.mediaRecordingVibrancyContainer, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) - //self.fieldBackgroundTint.backgroundColor = .blue transition.setCornerRadius(layer: self.fieldBackgroundTint.layer, cornerRadius: headerHeight > 0.0 ? 18.0 : baseFieldHeight * 0.5) var textClippingFrame = rawFieldBackgroundFrame.offsetBy(dx: 0.0, dy: headerHeight) @@ -1559,9 +1555,9 @@ public final class MessageInputPanelComponent: Component { if isEditing || component.style == .story || component.style == .videoChat || component.style == .gift { placeholderOriginX = 16.0 } else { - placeholderOriginX = floorToScreenPixels(fieldBackgroundFrame.minX + (fieldBackgroundFrame.width - placeholderSize.width) / 2.0) + placeholderOriginX = floorToScreenPixels((fieldBackgroundFrame.width - placeholderSize.width) / 2.0) } - let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: headerHeight + floor((rawFieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize) + let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: headerHeight + floorToScreenPixels((rawFieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize) if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view { if vibrancyPlaceholderView.superview == nil { vibrancyPlaceholderView.layer.anchorPoint = CGPoint() @@ -1900,6 +1896,7 @@ public final class MessageInputPanelComponent: Component { transition: transition, component: AnyComponent(MessageInputActionButtonComponent( mode: attachmentButtonMode, + style: attachmentButtonMode != .attach ? .glass(isTinted: false) : .legacy, storyId: component.storyItem?.id, action: { [weak self] mode, action, sendAction in guard let self, let component = self.component, case .up = action else { @@ -1950,7 +1947,7 @@ public final class MessageInputPanelComponent: Component { } else { attachmentButtonPosition = size.height - insets.bottom - baseFieldHeight + attachmentButtonPosition } - let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX), y: attachmentButtonPosition), size: attachmentButtonSize) + let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX) - 2.0, y: attachmentButtonPosition), size: attachmentButtonSize) transition.setPosition(view: attachmentButtonView, position: attachmentButtonFrame.center) transition.setBounds(view: attachmentButtonView, bounds: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size)) transition.setAlpha(view: attachmentButtonView, alpha: attachmentVisible ? 1.0 : 0.0) diff --git a/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift b/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift index 6879454e18..5d175befa2 100644 --- a/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift +++ b/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift @@ -86,7 +86,7 @@ public final class MoreHeaderButton: HighlightableButtonNode { transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent( - name: "anim_moredots" + name: "anim_morewide" ), color: self.color )), diff --git a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift index b14965b311..c02b17c648 100644 --- a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift @@ -316,6 +316,7 @@ public final class MultiplexedVideoNode: ASDisplayNode, ASScrollViewDelegate { } self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.view.addGestureRecognizer(recognizer) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 6d9263e04f..79005d5964 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -122,7 +122,6 @@ swift_library( "//submodules/TranslateUI", "//submodules/UndoUI", "//submodules/MediaPlayer:UniversalMediaPlayer", - "//submodules/WebSearchUI", "//submodules/WebUI", "//submodules/TelegramUI/Components/MultiScaleTextNode", "//submodules/GridMessageSelectionNode", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift index 339eb595d4..203de0c50a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift @@ -703,6 +703,7 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDe } self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) self.addSubnode(self.floatingHeaderNode) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 484b7954ed..51cc9be59f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -29,7 +29,6 @@ import GalleryUI import LegacyUI import MapResourceToAvatarSizes import LegacyComponents -import WebSearchUI import LocationResources import LocationUI import Geocoding @@ -1089,7 +1088,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -1107,8 +1106,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.openUrl(url: url.url, concealed: url.concealed, external: url.external ?? false) }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else { @@ -1163,14 +1162,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro actionSheet?.dismissAnimated() if let strongSelf = self { if canOpenIn { - let actionSheet = OpenInActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { }) } }) strongSelf.view.endEditing(true) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + strongSelf.controller?.push(actionSheet) } else { strongSelf.context.sharedContext.applicationBindings.openUrl(url) } @@ -1223,7 +1222,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in @@ -5385,7 +5384,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var contentHeight: CGFloat = 0.0 let sectionInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sectionInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { sectionInset = 0.0 @@ -5791,7 +5790,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if !additive { let sectionInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sectionInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { sectionInset = 0.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 5ed112284f..754acfe19a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -73,7 +73,7 @@ public extension PeerInfoScreenImpl { var avatarPickerHolder: Any? let _ = avatarPickerHolder - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: PeerType.getType(for: peer), getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { }, completion: { [weak parentController] result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in avatarPickerHolder = nil @@ -511,7 +511,7 @@ extension PeerInfoScreenImpl { let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController var dismissImpl: (() -> Void)? - let (mainController, pickerHolder) = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in + let (mainController, pickerHolder) = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, peerType: PeerType.getType(for: peer), getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in self?.openAvatarRemoval(mode: mode, peer: peer, item: item) }, completion: { [weak self] result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift index 47d6a0ca5c..6419048931 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift @@ -5,6 +5,7 @@ import AccountContext import SwiftSignalKit import TelegramCore import LegacyMediaPickerUI +import MediaPickerUI import ChatHistorySearchContainerNode import ChatScheduleTimeController import MediaResources @@ -229,6 +230,8 @@ extension PeerInfoScreenNode { transitionCompletion() }, getCaptionPanelView: { return nil + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == strongSelf.context.account.peerId, presentSchedulePicker: { [weak self] _, done in self?.presentMediaScheduleTimePicker(completion: { time, silentPosting in done(time, silentPosting) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift index 2211ef15b2..858c399463 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift @@ -80,13 +80,13 @@ extension PeerInfoScreenNode { } func openUrlIn(_ url: String) { - let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { }) } }) - self.controller?.present(actionSheet, in: .window(.root)) + self.controller?.push(actionSheet) } func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index b3d43bb29d..b51b155ebe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -251,12 +251,12 @@ public final class StorySearchGridScreen: ViewControllerComponentContainer { } self.present(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .mapMedia(locationMap), externalShare: true)), in: .window(.root), with: nil) }) - self.present(OpenInActionSheetController(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in + self.push(OpenInOptionsScreen(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in guard let self else { return } self.context.sharedContext.applicationBindings.openUrl(url) - }), in: .window(.root), with: nil) + })) } func updateTitle() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift index 7ab5a598b0..fe41b91c02 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift @@ -266,7 +266,7 @@ public final class AddGiftsScreen: ViewControllerComponentContainer { } self.filterButton.addTarget(self, action: #selector(self.filterPressed), forControlEvents: .touchUpInside) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.filterButton) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 0f305483a0..e1c98a1ac3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -819,7 +819,7 @@ final class GiftsListView: UIView { environment: {}, containerSize: CGSize(width: params.size.width - sideInset * 2.0, height: params.size.height) ) - let buttonAttributedString = NSAttributedString(string: presentationData.strings.PeerInfo_Gifts_EmptyCollection_Action, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let buttonAttributedString = NSAttributedString(string: presentationData.strings.PeerInfo_Gifts_EmptyCollection_Action, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let emptyResultsActionSize = self.emptyResultsAction.update( transition: .immediate, component: AnyComponent( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 1e7f1d5365..ee9ea2d9fc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -212,6 +212,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.delegate = self + self.scrollNode.view.scrollsToTop = false if let tabSelectorView = self.tabSelector.view { self.scrollNode.view.insertSubview(self.giftsListView, aboveSubview: tabSelectorView) @@ -783,7 +784,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr buttonTitle = params.presentationData.strings.PeerInfo_Gifts_SendGift } - let buttonAttributedString = NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let buttonAttributedString = NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) var buttonTitleContent: AnyComponent = AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) if let buttonIconName { buttonTitleContent = AnyComponent(HStack([ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 367f40270f..921303febf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1526,7 +1526,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var address: String? var distance: Double? var drivingTime: ExpectedTravelTime - var transitTime: ExpectedTravelTime var walkingTime: ExpectedTravelTime var hasEta: Bool @@ -1535,7 +1534,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: String?, distance: Double?, drivingTime: ExpectedTravelTime, - transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool ) { @@ -1543,7 +1541,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.address = address self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.hasEta = hasEta } @@ -1561,7 +1558,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let contextGestureContainerNode: ContextControllerSourceNode - private var mapOptionsNode: LocationOptionsNode? private var mapNode: LocationMapHeaderNode? private var mapDisposable: Disposable? @@ -2191,7 +2187,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .location = scope { let mapNode = LocationMapHeaderNode( presentationData: self.presentationData, - glass: false, + glass: true, toggleMapModeSelection: { [weak self] in guard let self else { return @@ -2201,7 +2197,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr state.displayingMapModeOptions = !state.displayingMapModeOptions self.locationViewState = state }, - updateMapMode: { _ in + updateMapMode: { [weak self] mode in + guard let self else { + return + } + + var state = self.locationViewState + state.mapMode = mode + state.displayingMapModeOptions = false + self.locationViewState = state }, goToUserLocation: { [weak self] in guard let self else { @@ -2333,7 +2337,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme) - strongSelf.mapOptionsNode?.updatePresentationData(presentationData) }) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) @@ -2354,24 +2357,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr throttledUserLocation(mapNode.mapNode.userLocation) ) - var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating)) var address: Signal = .single(nil) let locale = localeWithStrings(self.presentationData.strings) - eta = .single((.calculating, .calculating, .calculating)) - |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .transit), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + eta = .single((.calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) }) /*if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { @@ -2416,8 +2416,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: address, distance: distance, drivingTime: eta.0, - transitTime: eta.1, - walkingTime: eta.2, + walkingTime: eta.1, hasEta: false ) @@ -3755,12 +3754,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: addressString, distance: distanceString, drivingTime: mapInfoData.drivingTime, - transitTime: mapInfoData.transitTime, walkingTime: mapInfoData.walkingTime, hasEta: mapInfoData.hasEta, action: {}, drivingAction: {}, - transitAction: {}, walkingAction: {} ) let (mapInfoLayout, mapInfoReadyAndApply) = mapInfoNode.asyncLayout()( @@ -4200,30 +4197,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.mapNode != nil { self.updateMapLayout(size: size, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, transition: transition) gridTopInset += self.effectiveMapHeight - - let mapOptionsNode: LocationOptionsNode - if let current = self.mapOptionsNode { - mapOptionsNode = current - } else { - mapOptionsNode = LocationOptionsNode(presentationData: self.presentationData, hasBackground: false, updateMapMode: { [weak self] mode in - guard let self else { - return - } - - var state = self.locationViewState - state.mapMode = mode - state.displayingMapModeOptions = false - self.locationViewState = state - }) - mapOptionsNode.clipsToBounds = true - self.mapOptionsNode = mapOptionsNode - self.parentController?.navigationBar?.additionalContentNode.addSubnode(mapOptionsNode) - } - - let mapOptionsFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - self.additionalNavigationHeight), size: CGSize(width: size.width, height: self.additionalNavigationHeight)) - transition.updatePosition(node: mapOptionsNode, position: mapOptionsFrame.center) - transition.updateBounds(node: mapOptionsNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: 38.0 - self.additionalNavigationHeight), size: mapOptionsFrame.size)) - mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) } var hasBarBackground = false diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index 09747a6867..7d549c6c61 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -102,8 +102,7 @@ private final class SheetContent: Component { self.environment = environment var contentHeight: CGFloat = 0.0 - - let titleString: String = environment.strings.ProfileLevelInfo_Title + let descriptionTextString: String var secondaryDescriptionTextString: String? if component.peer.id == component.context.account.peerId { @@ -178,7 +177,6 @@ private final class SheetContent: Component { )) } - let _ = titleString let titleSize = self.title.update( transition: transition, component: AnyComponent(AnimatedTextComponent( diff --git a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD index e9de562525..9d05ce1532 100644 --- a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/Components/LottieAnimationComponent", "//submodules/AccountContext", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD index 71952a3eca..6fec00f75a 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD @@ -23,7 +23,6 @@ swift_library( "//submodules/AppBundle", "//submodules/TelegramStringFormatting", "//submodules/PresentationDataUtils", - "//submodules/Components/SolidRoundedButtonComponent", "//submodules/Components/ResizableSheetComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/ButtonComponent", diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index 10c8b85c0d..a4e86a5cbe 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -20,7 +20,6 @@ import ButtonComponent import BundleIconComponent import GlassBarButtonComponent import MultilineTextComponent -import SolidRoundedButtonComponent private func sendInviteLinkHasInviteSection(subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer]) -> Bool { let premiumRestrictedUsers = peers.filter { peer in @@ -330,35 +329,39 @@ private final class SendInviteLinkContentComponent: Component { contentHeight += premiumTextSize.height contentHeight += 22.0 + let premiumButtonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let premiumButtonSize = premiumButton.update( transition: premiumItemsTransition, - component: AnyComponent(SolidRoundedButtonComponent( - title: environment.strings.SendInviteLink_SubscribeToPremiumButton, - badge: nil, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumButtonGradientColors[0], + foreground: .white, + pressedColor: premiumButtonGradientColors[0], + isShimmering: false, + gradient: ButtonComponent.Background.Gradient(colors: premiumButtonGradientColors) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(environment.strings.SendInviteLink_SubscribeToPremiumButton), + component: AnyComponent(ButtonTextContentComponent( + text: environment.strings.SendInviteLink_SubscribeToPremiumButton, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumButtonGradientColors[0] + )) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - animationName: nil, - iconPosition: .right, - iconSpacing: 4.0, action: { component.openPremium() } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.0) ) let premiumButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumButtonSize) if let premiumButtonView = premiumButton.view { diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 4388ed3db6..3e66115baf 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -219,7 +219,13 @@ final class BusinessLocationSetupScreenComponent: Component { guard let component = self.component else { return } - let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in + let controller = LocationPickerController( + context: component.context, + style: .glass, + updatedPresentationData: nil, + mode: .pick, + initialLocation: initialLocation, + completion: { [weak self] location, _, _, address, _ in guard let self else { return } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index bae7fd368b..0b14eda092 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -825,20 +825,21 @@ final class ChatbotSetupScreenComponent: Component { self.botResolutionState = botResolutionState self.botRights = [.reply, .readMessages, .deleteSentMessages, .deleteReceivedMessages] self.state?.updated(transition: .spring(duration: 0.3)) + + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, cancel: nil, destructive: false), + elevatedLayout: false, + position: .bottom, + animateInAsReplacement: false, + action: { _ in return true } + ), in: .current) } else { self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.ChatbotSetup_ErrorBotNotBusinessCapable, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { }) ]), in: .window(.root)) } - controller.present(UndoOverlayController( - presentationData: presentationData, - content: .actionSucceeded(title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, cancel: nil, destructive: false), - elevatedLayout: false, - position: .bottom, - animateInAsReplacement: false, - action: { _ in return true } - ), in: .current) } }, removeAction: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift index 635a2eb95d..3de44cb9bf 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -451,7 +451,7 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { var listInsets = layout.insets(options: [.input]) listInsets.top += navigationBarHeight - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) listInsets.left += inset listInsets.right += inset diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index b929f99a2a..b3a1c22249 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -1253,7 +1253,7 @@ final class UserAppearanceScreenComponent: Component { } contentHeight += profilePreviewSize.height - 38.0 - let previewEdgeEffectFrame = CGRect(origin: CGPoint(x: profilePreviewFrame.minX, y: profilePreviewFrame.maxY - 32.0), size: CGSize(width: profilePreviewFrame.width, height: previewEdgeEffectHeight)) + let previewEdgeEffectFrame = CGRect(origin: CGPoint(x: profilePreviewFrame.minX, y: profilePreviewFrame.maxY - 35.0), size: CGSize(width: profilePreviewFrame.width, height: previewEdgeEffectHeight)) previewTransition.setFrame(view: self.previewEdgeEffectView, frame: previewEdgeEffectFrame) self.previewEdgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: previewEdgeEffectFrame, edge: .top, edgeSize: previewEdgeEffectFrame.height, transition: previewTransition) previewTransition.setAlpha(view: self.previewEdgeEffectView, alpha: !self.scrolledUp ? 1.0 : 0.0) @@ -1276,7 +1276,8 @@ final class UserAppearanceScreenComponent: Component { return (TelegramTextAttributes.URL, contents) } ) - let previewFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.ProfileColorSetup_ProfileColorPreviewInfo, attributes: footerAttributes)) + let previewFooterRawText = environment.strings.ProfileColorSetup_ProfileColorPreviewInfo.replacingOccurrences(of: " >]", with: "\u{00A0}>]") + let previewFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(previewFooterRawText, attributes: footerAttributes)) if let range = previewFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { previewFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: previewFooterText.string)) } diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift index c9e6afe3e3..c4247157e5 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift @@ -331,7 +331,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)) } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 1.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame var animationContent: EmojiStatusComponent.AnimationContent? diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index b4a5c4fe28..7b7523dfee 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -320,7 +320,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers, topPeers: [])) } - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true, isStandalone: false, rank: nil, rankRole: nil) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: item.reaction == nil, isStandalone: false, rank: nil, rankRole: nil) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD index 49b5ed0d22..98a1be1ec5 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", "//submodules/AccountContext", + "//submodules/ItemListUI", "//submodules/PresentationDataUtils", "//submodules/WallpaperBackgroundNode", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift index 3b656ee5be..854e812593 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ItemListUI import PresentationDataUtils import MediaResources import WallpaperGalleryScreen @@ -57,7 +58,7 @@ public final class ThemeAccentColorController: ViewController { return self._ready } - private let segmentedTitleView: ThemeColorSegmentedTitleView + private let segmentedTitleView: ItemListControllerSegmentedTitleView private var applyDisposable = MetaDisposable() @@ -72,7 +73,11 @@ public final class ThemeAccentColorController: ViewController { let section: ThemeColorSection = .background self.section = section - self.segmentedTitleView = ThemeColorSegmentedTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings, selectedSection: section) + self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [ + self.presentationData.strings.Theme_Colors_Background, + self.presentationData.strings.Theme_Colors_Accent, + self.presentationData.strings.Theme_Colors_Messages + ], selectedIndex: section.rawValue) if case .background = mode { self.initialBackgroundColor = randomBackgroundColors.randomElement().flatMap { UIColor(rgb: UInt32(bitPattern: $0)) } @@ -80,38 +85,43 @@ public final class ThemeAccentColorController: ViewController { self.initialBackgroundColor = nil } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)) + super.init(navigationBarPresentationData: nil) //NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) self.navigationPresentation = .modal + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.segmentedTitleView.sectionUpdated = { [weak self] section in - if let strongSelf = self { - strongSelf.controllerNode.updateSection(section) - } - } - - self.segmentedTitleView.shouldUpdateSection = { [weak self] section, f in - guard let strongSelf = self else { - f(false) + self.segmentedTitleView.indexUpdated = { [weak self] index in + guard let strongSelf = self, let section = ThemeColorSection(rawValue: index) else { return } + if strongSelf.segmentedTitleView.index == index { + return + } + + let updateSection: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.segmentedTitleView.index = index + strongSelf.controllerNode.updateSection(section) + } + guard section == .background else { - f(true) + updateSection() return } if strongSelf.controllerNode.requiresWallpaperChange { let controller = textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarning, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - f(false) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarningProceed, action: { - f(true) + updateSection() })]) strongSelf.present(controller, in: .window(.root)) } else { - f(true) + updateSection() } } @@ -119,8 +129,7 @@ public final class ThemeAccentColorController: ViewController { self.title = self.presentationData.strings.Wallpaper_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } else { - self.navigationItem.titleView = self.segmentedTitleView - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + //self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } } @@ -373,13 +382,12 @@ public final class ThemeAccentColorController: ViewController { }, ready: self._ready) self.controllerNode.themeUpdated = { [weak self] theme in if let strongSelf = self { - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: theme, presentationStrings: strongSelf.presentationData.strings), transition: .immediate) strongSelf.segmentedTitleView.theme = theme } } self.controllerNode.requestSectionUpdate = { [weak self] section in if let strongSelf = self { - strongSelf.segmentedTitleView.setIndex(section.rawValue, animated: true) + strongSelf.segmentedTitleView.index = section.rawValue } } @@ -634,6 +642,25 @@ public final class ThemeAccentColorController: ViewController { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + let navigationLayout = self.navigationLayout(layout: layout) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout.navigationFrame.maxY, transition: transition) + + if case .background = self.mode { + if self.segmentedTitleView.superview != nil { + self.segmentedTitleView.removeFromSuperview() + } + } else { + if self.segmentedTitleView.superview == nil { + self.view.addSubview(self.segmentedTitleView) + } + + let segmentedTitleFrame = CGRect( + x: layout.safeInsets.left, + y: navigationLayout.navigationFrame.maxY - navigationLayout.defaultContentHeight, + width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, + height: navigationLayout.defaultContentHeight + ) + transition.updateFrame(view: self.segmentedTitleView, frame: segmentedTitleFrame) + } } } diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 781de809fe..6a0523e9bd 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -360,6 +360,9 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.backgroundContainerNode.addSubnode(self.backgroundWrapperNode) self.backgroundWrapperNode.addSubnode(self.backgroundNode) + self.patternButtonNode.dark = true + self.colorsButtonNode.dark = true + self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) self.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) self.playButtonNode.addTarget(self, action: #selector(self.playPressed), forControlEvents: .touchUpInside) @@ -601,8 +604,6 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate if let strongSelf = self { strongSelf.patternPanelNode.serviceBackgroundColor = color strongSelf.pageControlBackgroundNode.backgroundColor = color - strongSelf.patternButtonNode.buttonColor = color - strongSelf.colorsButtonNode.buttonColor = color strongSelf.playButtonBackgroundNode.updateColor(color: color, transition: .immediate) } }) @@ -625,6 +626,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false + self.pageControlNode.setPage(0.0) self.colorPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true self.patternPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true @@ -1147,7 +1150,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.messageNodes = messageNodes } - let toolbarHeight = 49.0 + layout.intrinsicInsets.bottom + let toolbarHeight = 52.0 + layout.intrinsicInsets.bottom var relativeOffset: CGFloat = 0.0 if !self.state.colorPanelCollapsed && self.state.section == .messages { relativeOffset = (CGFloat(self.state.selectedColor) / CGFloat(max(1, self.state.messagesColors.count))) * (self.colorPanelNode.frame.height + toolbarHeight + 144.0) * -1.0 @@ -1208,13 +1211,13 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate if case let .peer(peer) = self.resultMode, case .user = peer, !self.state.displayPatternPanel { toolbarBottomInset += 58.0 } - let toolbarHeight = 49.0 + toolbarBottomInset + let toolbarHeight = 52.0 + toolbarBottomInset transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) - self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition) + self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 52.0), layout: layout, transition: transition) var bottomInset = toolbarHeight - let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) - let inputFieldPanelHeight: CGFloat = 47.0 + let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) + 17.0 + let inputFieldPanelHeight = WallpaperColorPanelNode.topControlsHeight(for: layout.size.width) let colorPanelHeight = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + inputFieldPanelHeight var colorPanelOffset: CGFloat = 0.0 @@ -1250,7 +1253,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate transition.updateBounds(node: self.messagesContainerNode, bounds: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)) transition.updatePosition(node: self.messagesContainerNode, position: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height).center) - let backgroundSize = CGSize(width: bounds.width, height: bounds.height - (colorPanelHeight - colorPanelOffset)) + let backgroundSize = CGSize(width: bounds.width, height: bounds.height - (colorPanelHeight - colorPanelOffset) + 20.0) transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(), size: backgroundSize)) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift deleted file mode 100644 index a8d182994d..0000000000 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import UIKit -import SegmentedControlNode -import TelegramPresentationData - -final class ThemeColorSegmentedTitleView: UIView { - private let segmentedControlNode: SegmentedControlNode - - var theme: PresentationTheme { - didSet { - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) - } - } - - var index: Int { - get { - return self.segmentedControlNode.selectedIndex - } - set { - self.segmentedControlNode.selectedIndex = newValue - } - } - - func setIndex(_ index: Int, animated: Bool) { - self.segmentedControlNode.setSelectedIndex(index, animated: animated) - } - - var sectionUpdated: ((ThemeColorSection) -> Void)? - var shouldUpdateSection: ((ThemeColorSection, @escaping (Bool) -> Void) -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings, selectedSection: ThemeColorSection) { - self.theme = theme - - let sections = [strings.Theme_Colors_Background, strings.Theme_Colors_Accent, strings.Theme_Colors_Messages] - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: sections.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedSection.rawValue) - - super.init(frame: CGRect()) - - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let section = ThemeColorSection(rawValue: index) { - self?.sectionUpdated?(section) - } - } - - self.segmentedControlNode.selectedIndexShouldChange = { [weak self] index, f in - if let section = ThemeColorSection(rawValue: index) { - self?.shouldUpdateSection?(section, f) - } else { - f(false) - } - } - - self.addSubnode(self.segmentedControlNode) - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - let size = self.bounds.size - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width + 20.0), transition: .immediate) - self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: floor((size.height - controlSize.height) / 2.0)), size: controlSize) - } -} diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index fe727d90ef..3c2f83417d 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -659,7 +659,7 @@ public class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD index 45b7b88b4e..9737db2810 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD @@ -32,6 +32,9 @@ swift_library( "//submodules/LegacyMediaPickerUI", "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift index b4f8d02740..35fbca796e 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift @@ -3,31 +3,25 @@ import UIKit import AsyncDisplayKit import SwiftSignalKit import Display +import ComponentFlow import TelegramPresentationData import HexColor +import GlassBackgroundComponent -private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? -private func textInputBackgroundImage(fieldColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { - if let current = currentTextInputBackgroundImage { - if current.0.isEqual(fieldColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) { - return current.3 - } +private final class ColorPanelGlassBackgroundNode: ASDisplayNode { + override init() { + super.init() + + self.setViewBlock({ + return GlassBackgroundView() + }) + self.isUserInteractionEnabled = false } - - let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in - context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setFillColor(fieldColor.cgColor) - context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setStrokeColor(strokeColor.cgColor) - let strokeWidth: CGFloat = 1.0 - context.setLineWidth(strokeWidth) - context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) - })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) - if let image = image { - currentTextInputBackgroundImage = (fieldColor, strokeColor, diameter, image) - return image - } else { - return nil + + func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, isInteractive: Bool = false, transition: ContainedViewLayoutTransition) { + if let view = self.view as? GlassBackgroundView { + view.update(size: size, cornerRadius: cornerRadius, isDark: isDark, tintColor: .init(kind: .panel), isInteractive: isInteractive, transition: ComponentTransition(transition)) + } } } @@ -37,22 +31,23 @@ private func generateSwatchBorderImage(theme: PresentationTheme) -> UIImage? { private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme - + private let swatchNode: ASDisplayNode + private let swatchBackgroundNode: ColorPanelGlassBackgroundNode private let borderNode: ASImageNode private let removeButton: HighlightableButtonNode - private let textBackgroundNode: ASImageNode + private let textBackgroundNode: ColorPanelGlassBackgroundNode private let selectionNode: ASDisplayNode let textFieldNode: TextFieldNode private let measureNode: ImmediateTextNode private let prefixNode: ASTextNode - + private var gestureRecognizer: UITapGestureRecognizer? - + var colorChanged: ((UIColor, Bool) -> Void)? var colorRemoved: (() -> Void)? var colorSelected: (() -> Void)? - + private var color: UIColor? private var isDefault = false { @@ -60,69 +55,70 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.updateSelectionVisibility() } } - + var isRemovable: Bool = false { didSet { self.removeButton.isUserInteractionEnabled = self.isRemovable } } - + private var previousIsDefault: Bool? private var previousColor: UIColor? private var validLayout: (CGSize, Bool)? - + private var skipEndEditing = false private let displaySwatch: Bool - + init(theme: PresentationTheme, displaySwatch: Bool = true) { self.theme = theme self.displaySwatch = displaySwatch - - self.textBackgroundNode = ASImageNode() - self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.chat.inputPanel.inputBackgroundColor, strokeColor: theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) - self.textBackgroundNode.displayWithoutProcessing = true - self.textBackgroundNode.displaysAsynchronously = false - + + self.textBackgroundNode = ColorPanelGlassBackgroundNode() + self.selectionNode = ASDisplayNode() self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) self.selectionNode.cornerRadius = 3.0 self.selectionNode.isUserInteractionEnabled = false - + self.textFieldNode = TextFieldNode() self.measureNode = ImmediateTextNode() - + self.prefixNode = ASTextNode() self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) - + self.swatchNode = ASDisplayNode() self.swatchNode.cornerRadius = 10.5 - + self.swatchNode.clipsToBounds = true + + self.swatchBackgroundNode = ColorPanelGlassBackgroundNode() + self.borderNode = ASImageNode() self.borderNode.displaysAsynchronously = false self.borderNode.displayWithoutProcessing = true self.borderNode.image = generateSwatchBorderImage(theme: theme) - + self.removeButton = HighlightableButtonNode() self.removeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRemoveIcon"), color: theme.chat.inputPanel.inputControlColor), for: .normal) - + super.init() - + self.addSubnode(self.textBackgroundNode) self.addSubnode(self.selectionNode) self.addSubnode(self.textFieldNode) self.addSubnode(self.prefixNode) + self.addSubnode(self.swatchBackgroundNode) self.addSubnode(self.swatchNode) self.addSubnode(self.borderNode) self.addSubnode(self.removeButton) - + self.removeButton.addTarget(self, action: #selector(self.removePressed), forControlEvents: .touchUpInside) } - + override func didLoad() { super.didLoad() - + self.textFieldNode.textField.font = Font.regular(17.0) self.textFieldNode.textField.textColor = self.theme.chat.inputPanel.inputTextColor self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance @@ -134,26 +130,24 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) self.textFieldNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor - + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:))) self.view.addGestureRecognizer(gestureRecognizer) self.gestureRecognizer = gestureRecognizer } - + func updateTheme(_ theme: PresentationTheme) { self.theme = theme - - self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.chat.inputPanel.inputBackgroundColor, strokeColor: self.theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) - + self.textFieldNode.textField.textColor = self.isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor - + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) self.borderNode.image = generateSwatchBorderImage(theme: theme) self.updateBorderVisibility() } - + func setColor(_ color: UIColor, isDefault: Bool = false, update: Bool = true, ended: Bool = true) { self.color = color self.isDefault = isDefault @@ -169,7 +163,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.swatchNode.backgroundColor = color self.updateBorderVisibility() } - + private func updateBorderVisibility() { guard let color = self.swatchNode.backgroundColor else { return @@ -181,21 +175,21 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.borderNode.alpha = 0.0 } } - + @objc private func removePressed() { if self.textFieldNode.textField.isFirstResponder { self.skipEndEditing = true } - + self.colorRemoved?() } - + @objc private func tapped(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.colorSelected?() } } - + @objc internal 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) @@ -205,28 +199,28 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { textField.text = updated.uppercased() textField.textColor = self.theme.chat.inputPanel.inputTextColor - + if updated.count == 6, let color = UIColor(hexString: updated) { self.setColor(color) } - + if let (size, _) = self.validLayout { self.updateSelectionLayout(size: size, transition: .immediate) } } return false } - + @objc func textFieldTextChanged(_ sender: UITextField) { if let color = self.colorFromCurrentText() { self.setColor(color) } - + if let (size, _) = self.validLayout { self.updateSelectionLayout(size: size, transition: .immediate) } } - + @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.skipEndEditing = true if let color = self.colorFromCurrentText() { @@ -237,7 +231,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.resignFirstResponder() return false } - + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { self.skipEndEditing = false self.previousColor = self.color @@ -247,7 +241,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { return true } - + @objc func textFieldDidEndEditing(_ textField: UITextField) { if !self.skipEndEditing { if let color = self.colorFromCurrentText() { @@ -257,13 +251,13 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } } } - + func setSkipEndEditingIfNeeded() { if self.textFieldNode.textField.isFirstResponder && self.colorFromCurrentText() != nil { self.skipEndEditing = true } } - + private func colorFromCurrentText() -> UIColor? { if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) { return color @@ -271,43 +265,56 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { return nil } } - + private func updateSelectionLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.measureNode.attributedText = NSAttributedString(string: self.textFieldNode.textField.text ?? "", font: self.textFieldNode.textField.font) let size = self.measureNode.updateLayout(size) transition.updateFrame(node: self.selectionNode, frame: CGRect(x: self.textFieldNode.frame.minX, y: 6.0, width: max(0.0, size.width), height: 20.0)) } - + private func updateSelectionVisibility() { self.selectionNode.isHidden = true } - + func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, condensed) - - let swatchFrame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 21.0, height: 21.0)) + + let isDark = self.theme.overallDarkAppearance + transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + self.textBackgroundNode.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, transition: transition) + + let swatchInset: CGFloat = max(5.0, floor(size.height * 0.16)) + let swatchSize = CGSize(width: size.height - swatchInset * 2.0, height: size.height - swatchInset * 2.0) + let swatchFrame = CGRect(origin: CGPoint(x: swatchInset, y: swatchInset), size: swatchSize) + transition.updateFrame(node: self.swatchBackgroundNode, frame: swatchFrame) + self.swatchBackgroundNode.update(size: swatchFrame.size, cornerRadius: swatchFrame.height * 0.5, isDark: isDark, transition: transition) transition.updateFrame(node: self.swatchNode, frame: swatchFrame) transition.updateFrame(node: self.borderNode, frame: swatchFrame) + self.swatchNode.cornerRadius = swatchFrame.height * 0.5 + self.swatchBackgroundNode.isHidden = !self.displaySwatch self.swatchNode.isHidden = !self.displaySwatch - + self.borderNode.isHidden = !self.displaySwatch + let textPadding: CGFloat if self.displaySwatch { - textPadding = condensed ? 31.0 : 37.0 + textPadding = swatchFrame.maxX + (condensed ? 7.0 : 10.0) } else { - textPadding = 12.0 + textPadding = condensed ? 6.0 : 12.0 } - - transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: size.width - (21.0 + textPadding), height: size.height - 2.0)) - + + self.textFieldNode.textField.font = Font.regular(17.0) + self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) + + let removeSize = CGSize(width: size.height, height: size.height) + let removeOffset: CGFloat = condensed ? 2.0 : 0.0 + transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: max(1.0, size.width - textPadding - removeSize.width - 14.0), height: size.height - 2.0)) + self.updateSelectionLayout(size: size, transition: transition) - + let prefixSize = self.prefixNode.measure(size) - transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: 6.0), size: prefixSize)) - - let removeSize = CGSize(width: 33.0, height: 33.0) - let removeOffset: CGFloat = condensed ? 3.0 : 0.0 + transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: floor((size.height - prefixSize.height) / 2.0)), size: prefixSize)) + transition.updateFrame(node: self.removeButton, frame: CGRect(origin: CGPoint(x: size.width - removeSize.width + removeOffset, y: 0.0), size: removeSize)) self.removeButton.alpha = self.isRemovable ? 1.0 : 0.0 } @@ -322,7 +329,7 @@ public struct WallpaperColorPanelNodeState: Equatable { public var preview: Bool public var simpleGradientGeneration: Bool public var suggestedNewColor: HSBColor? - + public init(selection: Int? = nil, colors: [HSBColor], maximumNumberOfColors: Int, rotateAvailable: Bool, rotation: Int32, preview: Bool, simpleGradientGeneration: Bool, suggestedNewColor: HSBColor? = nil) { self.selection = selection self.colors = colors @@ -390,16 +397,14 @@ private final class ColorSampleItemNode: ASImageNode { public final class WallpaperColorPanelNode: ASDisplayNode { private var theme: PresentationTheme - + private var state: WallpaperColorPanelNodeState - - private let backgroundNode: NavigationBackgroundNode - private let topSeparatorNode: ASDisplayNode - private let bottomSeparatorNode: ASDisplayNode + private let rotateButton: HighlightableButtonNode private let swapButton: HighlightableButtonNode + private let sampleItemsBackgroundNode: ColorPanelGlassBackgroundNode + private let addButtonBackgroundNode: ColorPanelGlassBackgroundNode private let addButton: HighlightableButtonNode - private let doneButton: HighlightableButtonNode private let colorPickerNode: WallpaperColorPickerNode private var sampleItemNodes: [ColorSampleItemNode] = [] @@ -408,36 +413,32 @@ public final class WallpaperColorPanelNode: ASDisplayNode { public var colorsChanged: (([HSBColor], Int, Bool) -> Void)? public var colorSelected: (() -> Void)? public var rotate: (() -> Void)? - + public var colorAdded: (() -> Void)? public var colorRemoved: (() -> Void)? - + private var validLayout: (CGSize, CGFloat)? + public static func topControlsHeight(for width: CGFloat) -> CGFloat { + return 52.0 + } + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - - self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - self.bottomSeparatorNode = ASDisplayNode() - self.bottomSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - - self.doneButton = HighlightableButtonNode() - self.doneButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(theme), for: .normal) - + self.colorPickerNode = WallpaperColorPickerNode(strings: strings) - + self.rotateButton = HighlightableButtonNode() self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) self.swapButton = HighlightableButtonNode() self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.sampleItemsBackgroundNode = ColorPanelGlassBackgroundNode() + self.addButtonBackgroundNode = ColorPanelGlassBackgroundNode() self.addButton = HighlightableButtonNode() self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) self.multiColorFieldNode = ColorInputFieldNode(theme: theme, displaySwatch: false) - + self.state = WallpaperColorPanelNodeState( selection: 0, colors: [], @@ -447,22 +448,20 @@ public final class WallpaperColorPanelNode: ASDisplayNode { preview: false, simpleGradientGeneration: false ) - + super.init() - - self.backgroundColor = .white - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.bottomSeparatorNode) + + self.backgroundColor = .clear + + self.addSubnode(self.sampleItemsBackgroundNode) self.addSubnode(self.multiColorFieldNode) - self.addSubnode(self.doneButton) self.addSubnode(self.colorPickerNode) - + self.addSubnode(self.rotateButton) self.addSubnode(self.swapButton) + self.addSubnode(self.addButtonBackgroundNode) self.addSubnode(self.addButton) - + self.rotateButton.addTarget(self, action: #selector(self.rotatePressed), forControlEvents: .touchUpInside) self.swapButton.addTarget(self, action: #selector(self.swapPressed), forControlEvents: .touchUpInside) self.addButton.addTarget(self, action: #selector(self.addPressed), forControlEvents: .touchUpInside) @@ -496,7 +495,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { }, animated: strongSelf.state.colors.count >= 2) } } - + self.colorPickerNode.colorChanged = { [weak self] color in if let strongSelf = self { strongSelf.updateState({ current in @@ -522,22 +521,22 @@ public final class WallpaperColorPanelNode: ASDisplayNode { } } } - + public func updateTheme(_ theme: PresentationTheme) { self.theme = theme - self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) - self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor - self.bottomSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.multiColorFieldNode.updateTheme(theme) + self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) } - + public func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, animated: Bool = true) { var updateLayout = updateLayout let previousColors = self.state.colors let previousPreview = self.state.preview let previousSelection = self.state.selection self.state = f(self.state) - + let colorWasRemovable = self.multiColorFieldNode.isRemovable self.multiColorFieldNode.isRemovable = self.state.colors.count > 1 if colorWasRemovable != self.multiColorFieldNode.isRemovable { @@ -549,7 +548,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.colorPickerNode.color = self.state.colors[index] } } - + if updateLayout, let (size, bottomInset) = self.validLayout { self.updateLayout(size: size, bottomInset: bottomInset, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } @@ -570,36 +569,30 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.colorsChanged?(self.state.colors, self.state.selection ?? 0, !self.state.preview) } } - + public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, bottomInset) - - let condensedLayout = size.width < 375.0 - let separatorHeight = UIScreenPixel - let topPanelHeight: CGFloat = 47.0 - transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)) - self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) - transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: separatorHeight)) - transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight)) - - let fieldHeight: CGFloat = 33.0 - let leftInset: CGFloat - let rightInset: CGFloat - if condensedLayout { - leftInset = 6.0 - rightInset = 6.0 - } else { - leftInset = 15.0 - rightInset = 15.0 - } - - let buttonSize = CGSize(width: 26.0, height: 26.0) - let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors - transition.updateFrame(node: self.addButton, frame: CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize)) + let condensedLayout = size.width < 375.0 + let topPanelHeight = WallpaperColorPanelNode.topControlsHeight(for: size.width) + let controlsSize: CGFloat = 40.0 + let fieldHeight = controlsSize + let leftInset: CGFloat = 8.0 + let rightInset: CGFloat = leftInset + let sampleItemSpacing: CGFloat = 6.0 + + let buttonSize = CGSize(width: controlsSize, height: controlsSize) + let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors + let addButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + transition.updateFrame(node: self.addButtonBackgroundNode, frame: addButtonFrame) + self.addButtonBackgroundNode.update(size: buttonSize, cornerRadius: buttonSize.height * 0.5, isDark: self.theme.overallDarkAppearance, transition: transition) + transition.updateAlpha(node: self.addButtonBackgroundNode, alpha: canAddColors ? 1.0 : 0.0) + transition.updateSublayerTransformScale(node: self.addButtonBackgroundNode, scale: canAddColors ? 1.0 : 0.1) + transition.updateFrame(node: self.addButton, frame: addButtonFrame) transition.updateAlpha(node: self.addButton, alpha: canAddColors ? 1.0 : 0.0) transition.updateSublayerTransformScale(node: self.addButton, scale: canAddColors ? 1.0 : 0.1) - + func degreesToRadians(_ degrees: CGFloat) -> CGFloat { var degrees = degrees if degrees >= 270.0 { @@ -614,10 +607,12 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.swapButton.isHidden = true self.multiColorFieldNode.isHidden = false - let sampleItemSize: CGFloat = 32.0 - let sampleItemSpacing: CGFloat = 15.0 + let sampleCount = self.state.colors.count + let reservedAddWidth = canAddColors ? buttonSize.width + sampleItemSpacing : 0.0 + let sampleItemSize: CGFloat = 34.0 + let sampleItemsInset = floor((controlsSize - sampleItemSize) / 2.0) - var nextSampleX = leftInset + var nextSampleX = leftInset + sampleItemsInset for i in 0 ..< self.state.colors.count { var animateIn = false @@ -644,7 +639,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { if i != 0 { nextSampleX += sampleItemSpacing } - itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: (topPanelHeight - sampleItemSize) / 2.0), size: CGSize(width: sampleItemSize, height: sampleItemSize)) + itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: floor((topPanelHeight - sampleItemSize) / 2.0)), size: CGSize(width: sampleItemSize, height: sampleItemSize)) nextSampleX += sampleItemSize itemNode.update(size: itemNode.bounds.size, color: self.state.colors[i].color, isSelected: self.state.selection == i) @@ -665,17 +660,28 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.sampleItemNodes.removeSubrange(self.state.colors.count ..< self.sampleItemNodes.count) } - let fieldX = nextSampleX + sampleItemSpacing + if sampleCount > 0 { + let samplesBackgroundFrame = CGRect(x: leftInset, y: floor((topPanelHeight - controlsSize) / 2.0), width: max(controlsSize, nextSampleX - leftInset + sampleItemsInset), height: controlsSize) + transition.updateFrame(node: self.sampleItemsBackgroundNode, frame: samplesBackgroundFrame) + self.sampleItemsBackgroundNode.update(size: samplesBackgroundFrame.size, cornerRadius: controlsSize * 0.5, isDark: self.theme.overallDarkAppearance, transition: transition) + transition.updateAlpha(node: self.sampleItemsBackgroundNode, alpha: 1.0) + } else { + transition.updateFrame(node: self.sampleItemsBackgroundNode, frame: CGRect(x: leftInset, y: floor((topPanelHeight - controlsSize) / 2.0), width: controlsSize, height: controlsSize)) + transition.updateAlpha(node: self.sampleItemsBackgroundNode, alpha: 0.0) + } - let fieldFrame = CGRect(x: fieldX, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - fieldX - leftInset - (canAddColors ? (buttonSize.width + sampleItemSpacing) : 0.0), height: fieldHeight) + let fieldSpacing = sampleItemSpacing + 3.0 + let fieldX = nextSampleX + (sampleCount > 0 ? fieldSpacing : 0.0) + + let fieldFrame = CGRect(x: fieldX, y: floor((topPanelHeight - fieldHeight) / 2.0), width: max(1.0, size.width - fieldX - rightInset - reservedAddWidth), height: fieldHeight) transition.updateFrame(node: self.multiColorFieldNode, frame: fieldFrame) - self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: false, transition: transition) - - let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight - separatorHeight) - transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + separatorHeight), size: colorPickerSize)) - self.colorPickerNode.updateLayout(size: colorPickerSize, transition: transition) + self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: condensedLayout, transition: transition) + + let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight) + transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: colorPickerSize.width, height: colorPickerSize.height + bottomInset))) + self.colorPickerNode.updateLayout(size: colorPickerSize, bottomInset: bottomInset, transition: transition) } - + @objc private func rotatePressed() { self.rotate?() self.updateState({ current in @@ -688,7 +694,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { return updated }) } - + @objc private func swapPressed() { /*self.updateState({ current in var updated = current @@ -699,7 +705,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { return updated })*/ } - + @objc private func addPressed() { self.colorSelected?() self.colorAdded?() diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift index ec5198f61f..244b49cb9d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift @@ -21,7 +21,7 @@ private let knobBackgroundImage: UIImage? = { }() private let pointerImage: UIImage? = { - return generateImage(CGSize(width: 12.0, height: 55.0), opaque: false, scale: nil, rotatedContext: { size, context in + return generateImage(CGSize(width: 12.0, height: 60.0), opaque: false, scale: nil, rotatedContext: { size, context in context.setBlendMode(.clear) context.setFillColor(UIColor.clear.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -49,19 +49,6 @@ private let pointerImage: UIImage? = { }) }() -private let brightnessMaskImage: UIImage? = { - return generateImage(CGSize(width: 36.0, height: 36.0), opaque: false, scale: nil, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - - context.setFillColor(UIColor.white.cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: bounds) - })?.stretchableImage(withLeftCapWidth: 18, topCapHeight: 18) -}() - private let brightnessGradientImage: UIImage? = { return generateImage(CGSize(width: 160.0, height: 1.0), opaque: false, scale: nil, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -227,7 +214,6 @@ private final class WallpaperColorHueSaturationNode: ASDisplayNode { private final class WallpaperColorBrightnessNode: ASDisplayNode { private let gradientNode: ASImageNode - private let maskNode: ASImageNode var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { didSet { @@ -244,25 +230,17 @@ private final class WallpaperColorBrightnessNode: ASDisplayNode { self.gradientNode.displayWithoutProcessing = true self.gradientNode.image = brightnessGradientImage self.gradientNode.contentMode = .scaleToFill - - self.maskNode = ASImageNode() - self.maskNode.displaysAsynchronously = false - self.maskNode.displayWithoutProcessing = true - self.maskNode.image = brightnessMaskImage - self.maskNode.contentMode = .scaleToFill - + super.init() self.isOpaque = true self.addSubnode(self.gradientNode) - self.addSubnode(self.maskNode) } override func layout() { super.layout() self.gradientNode.frame = self.bounds - self.maskNode.frame = self.bounds } } @@ -335,21 +313,23 @@ final class WallpaperColorPickerNode: ASDisplayNode { init(strings: PresentationStrings) { self.brightnessNode = WallpaperColorBrightnessNode() + self.brightnessNode.clipsToBounds = true self.brightnessNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) self.brightnessKnobNode = ASImageNode() self.brightnessKnobNode.image = pointerImage self.brightnessKnobNode.isUserInteractionEnabled = false self.colorNode = WallpaperColorHueSaturationNode() self.colorNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) + self.colorNode.clipsToBounds = true self.colorKnobNode = WallpaperColorKnobNode() super.init() - self.backgroundColor = .white + self.backgroundColor = .clear + self.addSubnode(self.colorNode) self.addSubnode(self.brightnessNode) self.addSubnode(self.brightnessKnobNode) - self.addSubnode(self.colorNode) self.addSubnode(self.colorKnobNode) self.update() @@ -432,7 +412,7 @@ final class WallpaperColorPickerNode: ASDisplayNode { } private func update() { - self.backgroundColor = .white + self.backgroundColor = .clear self.colorNode.value = self.color.brightness self.brightnessNode.hsb = self.color.values self.colorKnobNode.color = self.color @@ -452,20 +432,29 @@ final class WallpaperColorPickerNode: ASDisplayNode { colorKnobFrame.origin = origin transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame) - let inset: CGFloat = 15.0 - let brightnessKnobSize = CGSize(width: 12.0, height: 55.0) + let inset: CGFloat = 16.0 + let brightnessKnobSize = CGSize(width: 12.0, height: 60.0) let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.color.brightness), y: size.height - 65.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame) } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, bottomInset: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { self.validLayout = size let colorHeight = size.height - 66.0 - transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight)) + let colorVisualHeight = colorHeight + bottomInset + transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorVisualHeight)) + self.colorNode.cornerRadius = min(32.0, colorVisualHeight * 0.5) + if #available(iOS 11.0, *) { + self.colorNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + if #available(iOS 13.0, *) { + self.colorNode.layer.cornerCurve = .continuous + } - let inset: CGFloat = 15.0 - transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 35.0)) + let inset: CGFloat = 16.0 + transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 40.0)) + self.brightnessNode.cornerRadius = 20.0 self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift index ca30053e7f..780aab8723 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift @@ -41,6 +41,7 @@ final class WallpaperCropNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.decelerationRate = UIScrollView.DecelerationRate.fast + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 6bd88ad0cd..beacb3441d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -169,12 +169,16 @@ class WallpaperGalleryInteraction { let beginTransitionToEditor: () -> Void let beginTransitionFromEditor: (Bool) -> Void let finishTransitionFromEditor: () -> Void + let toolbarCancel: () -> Void + let toolbarDone: (Bool) -> Void - init(editMedia: @escaping (PHAsset, UIImage, CGRect, TGMediaEditAdjustments?, UIView, @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, @escaping (UIImage?) -> Void) -> Void, beginTransitionToEditor: @escaping () -> Void, beginTransitionFromEditor: @escaping (Bool) -> Void, finishTransitionFromEditor: @escaping () -> Void) { + init(editMedia: @escaping (PHAsset, UIImage, CGRect, TGMediaEditAdjustments?, UIView, @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, @escaping (UIImage?) -> Void) -> Void, beginTransitionToEditor: @escaping () -> Void, beginTransitionFromEditor: @escaping (Bool) -> Void, finishTransitionFromEditor: @escaping () -> Void, toolbarCancel: @escaping () -> Void, toolbarDone: @escaping (Bool) -> Void) { self.editMedia = editMedia self.beginTransitionToEditor = beginTransitionToEditor self.beginTransitionFromEditor = beginTransitionFromEditor self.finishTransitionFromEditor = finishTransitionFromEditor + self.toolbarCancel = toolbarCancel + self.toolbarDone = toolbarDone } } @@ -214,14 +218,12 @@ public class WallpaperGalleryController: ViewController { private var previousCentralEntryIndex: Int? private let centralItemSubtitle = Promise() - private let centralItemStatus = Promise() private let centralItemAction = Promise() private let centralItemAttributesDisposable = DisposableSet(); private var validLayout: (ContainerViewLayout, CGFloat)? private var overlayNode: WallpaperGalleryOverlayNode? - private var toolbarNode: WallpaperGalleryToolbarNode? private var patternPanelNode: WallpaperPatternPanelNode? private var colorsPanelNode: WallpaperColorPanelNode? @@ -231,6 +233,8 @@ public class WallpaperGalleryController: ViewController { private var savedPatternWallpaper: TelegramWallpaper? private var savedPatternIntensity: Int32? + private var toolbarDoneIsSolid = false + private var toolbarDoneDismissed = false public var requiredLevel: Int? @@ -243,9 +247,10 @@ public class WallpaperGalleryController: ViewController { super.init(navigationBarPresentationData: nil) self.navigationPresentation = .modal + self._hasGlassStyle = true self.title = self.presentationData.strings.WallpaperPreview_Title - //self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.interaction = WallpaperGalleryInteraction(editMedia: { [weak self] asset, image, cropRect, adjustments, referenceView, apply, fullSizeApply in @@ -266,22 +271,11 @@ public class WallpaperGalleryController: ViewController { self.present(c, in: .window(.root)) } }) - }, beginTransitionToEditor: { [weak self] in - guard let self else { - return - } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - if let toolbarNode = self.toolbarNode { - transition.updateAlpha(node: toolbarNode, alpha: 0.0) - } + }, beginTransitionToEditor: { }, beginTransitionFromEditor: { [weak self] saving in guard let self else { return } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - if let toolbarNode = self.toolbarNode { - transition.updateAlpha(node: toolbarNode, alpha: 1.0) - } if let centralItemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { centralItemNode.beginTransitionFromEditor(saving: saving) } @@ -292,6 +286,10 @@ public class WallpaperGalleryController: ViewController { if let centralItemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { centralItemNode.finishTransitionFromEditor() } + }, toolbarCancel: { [weak self] in + self?.dismiss(forceAway: true) + }, toolbarDone: { [weak self] forBoth in + self?.toolbarDonePressed(forBoth: forBoth) }) var entries: [WallpaperGalleryEntry] = [] @@ -359,19 +357,6 @@ public class WallpaperGalleryController: ViewController { } })) - self.centralItemAttributesDisposable.add(self.centralItemStatus.get().start(next: { [weak self] status in - if let strongSelf = self { - let enabled: Bool - switch status { - case .Local: - enabled = true - default: - enabled = false - } - strongSelf.toolbarNode?.setDoneEnabled(enabled) - } - })) - self.centralItemAttributesDisposable.add(self.centralItemAction.get().start(next: { [weak self] barButton in if let strongSelf = self { strongSelf.navigationItem.rightBarButtonItem = barButton @@ -395,7 +380,7 @@ public class WallpaperGalleryController: ViewController { } self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData), transition: .immediate) - self.toolbarNode?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.updateVisibleItemToolbars(layout: self.validLayout?.0, transition: .immediate) self.patternPanelNode?.updateTheme(self.presentationData.theme) self.colorsPanelNode?.updateTheme(self.presentationData.theme) @@ -408,6 +393,293 @@ public class WallpaperGalleryController: ViewController { self.presentingViewController?.dismiss(animated: true, completion: nil) //self.galleryNode.modalAnimateOut(completion: completion) } + + private func toolbarHeight() -> CGFloat { + var toolbarHeight: CGFloat = 66.0 + if case let .peer(peer, _) = self.mode, case .user = peer { + toolbarHeight += 58.0 + } + return toolbarHeight + } + + private func toolbarDoneButtonType() -> WallpaperGalleryToolbarDoneButtonType { + var doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set + switch self.source { + case let .wallpaper(wallpaper, _, _, _, _, _): + switch wallpaper { + case let .file(file): + if file.id == 0 { + doneButtonType = .none + } + default: + break + } + default: + break + } + if case let .peer(peer, _) = self.mode { + if case .user = peer { + doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + } else { + doneButtonType = .setChannel + } + } + return doneButtonType + } + + private func itemArguments(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false) -> WallpaperGalleryItemArguments { + return WallpaperGalleryItemArguments( + colorPreview: colorPreview, + isColorsList: isColorsList, + patternEnabled: patternEnabled, + toolbarHeight: self.toolbarHeight(), + toolbarDoneButtonType: self.toolbarDoneButtonType(), + toolbarRequiredLevel: self.requiredLevel + ) + } + + private func updateVisibleItemToolbars(layout: ContainerViewLayout?, transition: ContainedViewLayoutTransition) { + guard self.isNodeLoaded else { + return + } + let toolbarHeight = self.toolbarHeight() + self.galleryNode.pager.forEachItemNode { itemNode in + guard let itemNode = itemNode as? WallpaperGalleryItemNode else { + return + } + itemNode.updateToolbarThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + itemNode.setToolbarDoneIsSolid(self.toolbarDoneIsSolid, transition: transition) + if let layout { + itemNode.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, transition: transition) + } + } + } + + private func toolbarDonePressed(forBoth: Bool) { + guard !self.toolbarDoneDismissed else { + return + } + + let strongSelf = self + if forBoth && !strongSelf.context.isPremium { + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.push(controller) + + return + } + + if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { + if centralItemNode.cropNode.scrollNode.view.isDecelerating { + return + } + strongSelf.toolbarDoneDismissed = true + let options = centralItemNode.options + if !strongSelf.entries.isEmpty { + let entry = strongSelf.entries[centralItemNode.index] + let apply = strongSelf.apply + if case .peer = strongSelf.mode { + if case let .wallpaper(wallpaper, _) = entry, options.contains(.blur) { + var resource: TelegramMediaResource? + switch wallpaper { + case let .file(file): + resource = file.file.resource + case let .image(representations, _): + if let largestSize = largestImageRepresentation(representations) { + resource = largestSize.resource + } + default: + break + } + if let resource = resource { + let representation = CachedBlurredWallpaperRepresentation() + var data: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + apply?(entry, options, nil, nil, centralItemNode.brightness, forBoth) + }) + } + } + } else { + apply?(entry, options, centralItemNode.editedFullSizeImage, centralItemNode.editedCropRect, centralItemNode.brightness, forBoth) + } + return + } + + switch entry { + case let .wallpaper(wallpaper, _): + var resource: TelegramMediaResource? + switch wallpaper { + case let .file(file): + resource = file.file.resource + case let .image(representations, _): + if let largestSize = largestImageRepresentation(representations) { + resource = largestSize.resource + } + default: + break + } + + let completion: (TelegramWallpaper) -> Void = { wallpaper in + let baseSettings = wallpaper.settings + let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: baseSettings?.intensity, rotation: baseSettings?.rotation) + let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) + + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered + let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + let wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + let accentColor = current.themeSpecificAccentColors[themeReference.index] + if let accentColor = accentColor, accentColor.baseColor == .custom { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper + } else { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil + themeSpecificChatWallpapers[themeReference.index] = wallpaper + } + return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) + }) |> deliverOnMainQueue).start(completed: { + strongSelf.dismiss(animated: true) + }) + + switch strongSelf.source { + case .wallpaper, .slug: + let _ = saveWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() + default: + break + } + let _ = installWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in + WallpapersState.update(transaction: transaction, { state in + var state = state + if let index = state.wallpapers.firstIndex(where: { + $0.isBasicallyEqual(to: wallpaper) + }) { + state.wallpapers.remove(at: index) + } + state.wallpapers.insert(wallpaper, at: 0) + return state + }) + }).start() + } + + let applyWallpaper: (TelegramWallpaper) -> Void = { wallpaper in + if options.contains(.blur) { + if let resource = resource { + let representation = CachedBlurredWallpaperRepresentation() + var data: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + completion(wallpaper) + }) + } + } + } else if case let .file(file) = wallpaper, let resource = resource { + if wallpaper.isPattern, !file.settings.colors.isEmpty, let _ = file.settings.intensity { + var data: Data? + var thumbnailData: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + let thumbnailResource = file.file.previewRepresentations.first?.resource + + if let resource = thumbnailResource { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) + } + completion(wallpaper) + } + } else if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) + completion(wallpaper) + } + } else { + completion(wallpaper) + } + } + + if case let .image(currentRepresentations, currentSettings) = wallpaper { + let _ = (strongSelf.context.wallpaperUploadManager!.stateSignal() + |> take(1) + |> deliverOnMainQueue).start(next: { status in + switch status { + case let .uploaded(uploadedWallpaper, resultWallpaper): + if case let .image(uploadedRepresentations, _) = uploadedWallpaper, uploadedRepresentations == currentRepresentations { + let updatedWallpaper = resultWallpaper.withUpdatedSettings(currentSettings) + applyWallpaper(updatedWallpaper) + return + } + case let .uploading(uploadedWallpaper, _): + if case let .image(uploadedRepresentations, uploadedSettings) = uploadedWallpaper, uploadedRepresentations == currentRepresentations, uploadedSettings != currentSettings { + let updatedWallpaper = uploadedWallpaper.withUpdatedSettings(currentSettings) + applyWallpaper(updatedWallpaper) + return + } + default: + break + } + applyWallpaper(wallpaper) + }) + } else { + var updatedWallpaper = wallpaper + if var settings = wallpaper.settings { + settings.motion = options.contains(.motion) + updatedWallpaper = updatedWallpaper.withUpdatedSettings(settings) + } + applyWallpaper(updatedWallpaper) + } + default: + break + } + + strongSelf.apply?(entry, options, nil, centralItemNode.cropRect, centralItemNode.brightness, forBoth) + } + } + } private func updateTransaction(entries: [WallpaperGalleryEntry], arguments: WallpaperGalleryItemArguments) -> GalleryPagerTransaction { var i: Int = 0 @@ -479,271 +751,10 @@ public class WallpaperGalleryController: ViewController { break } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let overlayNode = WallpaperGalleryOverlayNode() self.overlayNode = overlayNode self.galleryNode.overlayNode = overlayNode self.galleryNode.addSubnode(overlayNode) - - var doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set - switch self.source { - case let .wallpaper(wallpaper, _, _, _, _, _): - switch wallpaper { - case let .file(file): - if file.id == 0 { - doneButtonType = .none - } - default: - break - } - default: - break - } - if case let .peer(peer, _) = self.mode { - if case .user = peer { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) - } else { - doneButtonType = .setChannel - } - } - - let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType) - toolbarNode.requiredLevel = self.requiredLevel - switch self.source { - case .asset, .contextResult: - toolbarNode.dark = false - default: - toolbarNode.dark = true - } - self.toolbarNode = toolbarNode - overlayNode.addSubnode(toolbarNode) - - toolbarNode.cancel = { [weak self] in - self?.dismiss(forceAway: true) - } - var dismissed = false - toolbarNode.done = { [weak self] forBoth in - if let strongSelf = self, !dismissed { - if forBoth && !strongSelf.context.isPremium { - let context = strongSelf.context - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.push(controller) - - return - } - - if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { - if centralItemNode.cropNode.scrollNode.view.isDecelerating { - return - } - dismissed = true - let options = centralItemNode.options - if !strongSelf.entries.isEmpty { - let entry = strongSelf.entries[centralItemNode.index] - let apply = strongSelf.apply - if case .peer = strongSelf.mode { - if case let .wallpaper(wallpaper, _) = entry, options.contains(.blur) { - var resource: TelegramMediaResource? - switch wallpaper { - case let .file(file): - resource = file.file.resource - case let .image(representations, _): - if let largestSize = largestImageRepresentation(representations) { - resource = largestSize.resource - } - default: - break - } - if let resource = resource { - let representation = CachedBlurredWallpaperRepresentation() - var data: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) - |> filter({ $0.complete }) - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - apply?(entry, options, nil, nil, centralItemNode.brightness, forBoth) - }) - } - } - } else { - apply?(entry, options, centralItemNode.editedFullSizeImage, centralItemNode.editedCropRect, centralItemNode.brightness, forBoth) - } - return - } - - switch entry { - case let .wallpaper(wallpaper, _): - var resource: TelegramMediaResource? - switch wallpaper { - case let .file(file): - resource = file.file.resource - case let .image(representations, _): - if let largestSize = largestImageRepresentation(representations) { - resource = largestSize.resource - } - default: - break - } - - let completion: (TelegramWallpaper) -> Void = { wallpaper in - let baseSettings = wallpaper.settings - let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: baseSettings?.intensity, rotation: baseSettings?.rotation) - let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) - - let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered - let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - let wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper - let themeReference: PresentationThemeReference - if autoNightModeTriggered { - themeReference = current.automaticThemeSwitchSetting.theme - } else { - themeReference = current.theme - } - let accentColor = current.themeSpecificAccentColors[themeReference.index] - if let accentColor = accentColor, accentColor.baseColor == .custom { - themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper - } else { - themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil - themeSpecificChatWallpapers[themeReference.index] = wallpaper - } - return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) - }) |> deliverOnMainQueue).start(completed: { - self?.dismiss(animated: true) - }) - - switch strongSelf.source { - case .wallpaper, .slug: - let _ = saveWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() - default: - break - } - let _ = installWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() - let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in - WallpapersState.update(transaction: transaction, { state in - var state = state - if let index = state.wallpapers.firstIndex(where: { - $0.isBasicallyEqual(to: wallpaper) - }) { - state.wallpapers.remove(at: index) - } - state.wallpapers.insert(wallpaper, at: 0) - return state - }) - }).start() - } - - let applyWallpaper: (TelegramWallpaper) -> Void = { wallpaper in - if options.contains(.blur) { - if let resource = resource { - let representation = CachedBlurredWallpaperRepresentation() - var data: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) - |> filter({ $0.complete }) - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - completion(wallpaper) - }) - } - } - } else if case let .file(file) = wallpaper, let resource = resource { - if wallpaper.isPattern, !file.settings.colors.isEmpty, let _ = file.settings.intensity { - var data: Data? - var thumbnailData: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - let thumbnailResource = file.file.previewRepresentations.first?.resource - - if let resource = thumbnailResource { - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - thumbnailData = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - thumbnailData = maybeData - } - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) - } - completion(wallpaper) - } - } else if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) - completion(wallpaper) - } - } else { - completion(wallpaper) - } - } - - if case let .image(currentRepresentations, currentSettings) = wallpaper { - let _ = (strongSelf.context.wallpaperUploadManager!.stateSignal() - |> take(1) - |> deliverOnMainQueue).start(next: { status in - switch status { - case let .uploaded(uploadedWallpaper, resultWallpaper): - if case let .image(uploadedRepresentations, _) = uploadedWallpaper, uploadedRepresentations == currentRepresentations { - let updatedWallpaper = resultWallpaper.withUpdatedSettings(currentSettings) - applyWallpaper(updatedWallpaper) - return - } - case let .uploading(uploadedWallpaper, _): - if case let .image(uploadedRepresentations, uploadedSettings) = uploadedWallpaper, uploadedRepresentations == currentRepresentations, uploadedSettings != currentSettings { - let updatedWallpaper = uploadedWallpaper.withUpdatedSettings(currentSettings) - applyWallpaper(updatedWallpaper) - return - } - default: - break - } - applyWallpaper(wallpaper) - }) - } else { - var updatedWallpaper = wallpaper - if var settings = wallpaper.settings { - settings.motion = options.contains(.motion) - updatedWallpaper = updatedWallpaper.withUpdatedSettings(settings) - } - applyWallpaper(updatedWallpaper) - } - default: - break - } - - strongSelf.apply?(entry, options, nil, centralItemNode.cropRect, centralItemNode.brightness, forBoth) - } - } - } - } } private func currentEntry() -> WallpaperGalleryEntry? { @@ -770,11 +781,11 @@ public class WallpaperGalleryController: ViewController { private func bindCentralItemNode(animated: Bool, updated: Bool) { if let node = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { self.centralItemSubtitle.set(node.subtitle.get()) - self.centralItemStatus.set(node.status.get()) self.centralItemAction.set(node.actionButton.get()) node.action = { [weak self] in self?.actionPressed() } + self.updateVisibleItemToolbars(layout: self.validLayout?.0, transition: .immediate) node.requestPatternPanel = { [weak self] enabled, initialWallpaper in if let strongSelf = self, let (layout, _) = strongSelf.validLayout { strongSelf.colorsPanelEnabled = false @@ -901,7 +912,7 @@ public class WallpaperGalleryController: ViewController { entries[centralEntryIndex] = currentEntry self.entries = entries - self.galleryNode.pager.transaction(self.updateTransaction(entries: entries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled))) + self.galleryNode.pager.transaction(self.updateTransaction(entries: entries, arguments: self.itemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled))) } private func updateEntries(wallpaper: TelegramWallpaper, preview: Bool = false) { @@ -920,7 +931,7 @@ public class WallpaperGalleryController: ViewController { entries[centralEntryIndex] = currentEntry self.entries = entries - self.galleryNode.pager.transaction(self.updateCurrentEntryTransaction(entry: currentEntry, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled), index: centralEntryIndex)) + self.galleryNode.pager.transaction(self.updateCurrentEntryTransaction(entry: currentEntry, arguments: self.itemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled), index: centralEntryIndex)) } private func updateEntries(pattern: TelegramWallpaper?, intensity: Int32? = nil, preview: Bool = false) { @@ -955,7 +966,7 @@ public class WallpaperGalleryController: ViewController { } self.entries = updatedEntries - self.galleryNode.pager.transaction(self.updateTransaction(entries: updatedEntries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: true, patternEnabled: self.patternPanelEnabled))) + self.galleryNode.pager.transaction(self.updateTransaction(entries: updatedEntries, arguments: self.itemArguments(colorPreview: preview, isColorsList: true, patternEnabled: self.patternPanelEnabled))) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -963,7 +974,9 @@ public class WallpaperGalleryController: ViewController { super.containerLayoutUpdated(layout, transition: transition) - let panelHeight: CGFloat = 235.0 + let panelHeight: CGFloat = 188.0 + WallpaperColorPanelNode.topControlsHeight(for: layout.size.width) + let toolbarHeight = self.toolbarHeight() + self.toolbarDoneIsSolid = self.patternPanelEnabled || self.colorsPanelEnabled var pagerLayout = layout if self.patternPanelEnabled || self.colorsPanelEnabled { @@ -974,13 +987,7 @@ public class WallpaperGalleryController: ViewController { self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.galleryNode.containerLayoutUpdated(pagerLayout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) self.overlayNode?.frame = self.galleryNode.bounds - - var toolbarHeight: CGFloat = 66.0 - if case let .peer(peer, _) = self.mode, case .user = peer { - toolbarHeight += 58.0 - } - transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom))) - self.toolbarNode!.updateLayout(size: CGSize(width: layout.size.width, height: toolbarHeight), layout: layout, transition: transition) + self.updateVisibleItemToolbars(layout: layout, transition: transition) var bottomInset = toolbarHeight + layout.intrinsicInsets.bottom @@ -1025,7 +1032,7 @@ public class WallpaperGalleryController: ViewController { } self.patternPanelNode = patternPanelNode currentPatternPanelNode = patternPanelNode - self.overlayNode?.insertSubnode(patternPanelNode, belowSubnode: self.toolbarNode!) + self.overlayNode?.addSubnode(patternPanelNode) } let currentColorsPanelNode: WallpaperColorPanelNode @@ -1035,7 +1042,7 @@ public class WallpaperGalleryController: ViewController { let colorsPanelNode = WallpaperColorPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings) self.colorsPanelNode = colorsPanelNode currentColorsPanelNode = colorsPanelNode - self.overlayNode?.insertSubnode(colorsPanelNode, belowSubnode: self.toolbarNode!) + self.overlayNode?.addSubnode(colorsPanelNode) colorsPanelNode.colorsChanged = { [weak self] colors, _, _ in guard let strongSelf = self else { @@ -1055,7 +1062,6 @@ public class WallpaperGalleryController: ViewController { } } - let originalBottomInset = bottomInset var patternPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) if self.patternPanelEnabled { patternPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - max((layout.inputHeight ?? 0.0) - panelHeight + 44.0, bottomInset) - panelHeight) @@ -1063,7 +1069,7 @@ public class WallpaperGalleryController: ViewController { } transition.updateFrame(node: currentPatternPanelNode, frame: patternPanelFrame) - currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, bottomInset: originalBottomInset, transition: transition) + currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, bottomInset: 0.0, transition: transition) var colorsPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) if self.colorsPanelEnabled { @@ -1071,11 +1077,9 @@ public class WallpaperGalleryController: ViewController { bottomInset += panelHeight } - transition.updateFrame(node: currentColorsPanelNode, frame: CGRect(origin: colorsPanelFrame.origin, size: CGSize(width: colorsPanelFrame.width, height: colorsPanelFrame.height + originalBottomInset))) - currentColorsPanelNode.updateLayout(size: colorsPanelFrame.size, bottomInset: originalBottomInset, transition: transition) + transition.updateFrame(node: currentColorsPanelNode, frame: colorsPanelFrame) + currentColorsPanelNode.updateLayout(size: colorsPanelFrame.size, bottomInset: 0.0, transition: transition) - self.toolbarNode?.setDoneIsSolid(self.patternPanelEnabled || self.colorsPanelEnabled, transition: transition) - bottomInset += toolbarHeight self.validLayout = (layout, bottomInset) @@ -1085,11 +1089,12 @@ public class WallpaperGalleryController: ViewController { colors = true } - self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: WallpaperGalleryItemArguments(isColorsList: colors), source: self.source, mode: self.mode, interaction: self.interaction!) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: self.itemArguments(isColorsList: colors), source: self.source, mode: self.mode, interaction: self.interaction!) }), centralItemIndex: self.centralEntryIndex) if let initialOptions = self.initialOptions, let itemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { itemNode.options = initialOptions } + self.updateVisibleItemToolbars(layout: layout, transition: .immediate) } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index cc1c15d67d..7e36181a6d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -20,16 +20,24 @@ import WallpaperBackgroundNode import TextFormat import TooltipUI import TelegramNotices +import ComponentFlow +import GlassControls struct WallpaperGalleryItemArguments { let colorPreview: Bool let isColorsList: Bool let patternEnabled: Bool + let toolbarHeight: CGFloat + let toolbarDoneButtonType: WallpaperGalleryToolbarDoneButtonType + let toolbarRequiredLevel: Int? - init(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false) { + init(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false, toolbarHeight: CGFloat = 66.0, toolbarDoneButtonType: WallpaperGalleryToolbarDoneButtonType = .set, toolbarRequiredLevel: Int? = nil) { self.colorPreview = colorPreview self.isColorsList = isColorsList self.patternEnabled = patternEnabled + self.toolbarHeight = toolbarHeight + self.toolbarDoneButtonType = toolbarDoneButtonType + self.toolbarRequiredLevel = toolbarRequiredLevel } } @@ -105,10 +113,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private let blurredNode: BlurredImageNode let cropNode: WallpaperCropNode - private let cancelButtonNode: WallpaperNavigationButtonNode - private let shareButtonNode: WallpaperNavigationButtonNode - private let dayNightButtonNode: WallpaperNavigationButtonNode - private let editButtonNode: WallpaperNavigationButtonNode + private let topButtons: ComponentView + private var topCanShare = false + private var topCanSwitchTheme = false + private var topCanEdit = false + private var controlsGlassIsDark = true private let buttonsContainerNode: SparseNode private let blurButtonNode: WallpaperOptionButtonNode @@ -117,6 +126,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private let colorsButtonNode: WallpaperOptionButtonNode private let playButtonNode: WallpaperNavigationButtonNode private let sliderNode: WallpaperSliderNode + private let toolbarNode: WallpaperGalleryToolbarNode + private var toolbarDoneEnabled = true private let messagesContainerNode: ASDisplayNode private var messageNodes: [ListViewItemNode]? @@ -138,6 +149,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var requestRotateGradient: ((Int32) -> Void)? private var validLayout: (ContainerViewLayout, CGFloat)? + private var validToolbarLayout: (ContainerViewLayout, CGFloat)? private var validOffset: CGFloat? private var initialWallpaper: TelegramWallpaper? @@ -176,6 +188,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.messagesContainerNode.isUserInteractionEnabled = false + self.topButtons = ComponentView() self.buttonsContainerNode = SparseNode() self.blurButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Blurred, value: .check(false)) self.blurButtonNode.setEnabled(false) @@ -192,16 +205,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { }) self.colorsButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_WallpaperColors, value: .colors(false, [.clear])) + self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.cancelButtonNode = WallpaperNavigationButtonNode(content: .text(self.presentationData.strings.Common_Cancel), dark: true) - self.cancelButtonNode.enableSaturation = true - self.shareButtonNode = WallpaperNavigationButtonNode(content: .icon(image: UIImage(bundleImageName: "Chat/Links/Share"), size: CGSize(width: 28.0, height: 28.0)), dark: true) - self.shareButtonNode.enableSaturation = true - self.dayNightButtonNode = WallpaperNavigationButtonNode(content: .dayNight(isNight: self.isDarkAppearance), dark: true) - self.dayNightButtonNode.enableSaturation = true - self.editButtonNode = WallpaperNavigationButtonNode(content: .icon(image: UIImage(bundleImageName: "Settings/WallpaperAdjustments"), size: CGSize(width: 28.0, height: 28.0)), dark: true) - self.editButtonNode.enableSaturation = true - self.playButtonPlayImage = generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) @@ -264,10 +269,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.buttonsContainerNode.addSubnode(self.colorsButtonNode) self.buttonsContainerNode.addSubnode(self.playButtonNode) self.buttonsContainerNode.addSubnode(self.sliderNode) - self.buttonsContainerNode.addSubnode(self.cancelButtonNode) - self.buttonsContainerNode.addSubnode(self.shareButtonNode) - self.buttonsContainerNode.addSubnode(self.dayNightButtonNode) - self.buttonsContainerNode.addSubnode(self.editButtonNode) + self.buttonsContainerNode.addSubnode(self.toolbarNode) self.imageNode.addSubnode(self.brightnessNode) @@ -276,10 +278,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) self.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) self.playButtonNode.addTarget(self, action: #selector(self.togglePlay), forControlEvents: .touchUpInside) - self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.shareButtonNode.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) - self.dayNightButtonNode.addTarget(self, action: #selector(self.dayNightPressed), forControlEvents: .touchUpInside) - self.editButtonNode.addTarget(self, action: #selector(self.editPressed), forControlEvents: .touchUpInside) sliderValueChangedImpl = { [weak self] value in if let self { @@ -459,7 +457,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { @objc private func dayNightPressed() { self.isDarkAppearance = !self.isDarkAppearance - self.dayNightButtonNode.setIsNight(self.isDarkAppearance) if let layout = self.validLayout?.0 { let offset = CGPoint(x: self.validOffset ?? 0.0, y: 0.0) @@ -594,6 +591,20 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.source = source self.mode = mode self.interaction = interaction + + self.toolbarNode.doneButtonType = arguments.toolbarDoneButtonType + self.toolbarNode.requiredLevel = arguments.toolbarRequiredLevel + self.updateToolbarThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.toolbarNode.cancel = { [weak self] in + if let interaction = self?.interaction { + interaction.toolbarCancel() + } + } + self.toolbarNode.done = { [weak self] forBoth in + if let interaction = self?.interaction { + interaction.toolbarDone(forBoth) + } + } if self.arguments.colorPreview != previousArguments.colorPreview { if self.arguments.colorPreview { @@ -696,16 +707,25 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.playButtonNode.setIcon(self.playButtonRotateImage) } - self.cancelButtonNode.enableSaturation = isColor - self.dayNightButtonNode.enableSaturation = isColor - self.editButtonNode.enableSaturation = isColor - self.shareButtonNode.enableSaturation = isColor self.patternButtonNode.backgroundNode.enableSaturation = isColor self.blurButtonNode.backgroundNode.enableSaturation = isColor self.motionButtonNode.backgroundNode.enableSaturation = isColor self.colorsButtonNode.backgroundNode.enableSaturation = isColor self.playButtonNode.enableSaturation = isColor - + + let controlsGlassIsDark = self.presentationData.theme.overallDarkAppearance //!isColor + self.controlsGlassIsDark = controlsGlassIsDark + self.patternButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.blurButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.motionButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.colorsButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.playButtonNode.dark = controlsGlassIsDark + self.toolbarNode.dark = controlsGlassIsDark + self.blurButtonNode.dark = controlsGlassIsDark + self.motionButtonNode.dark = controlsGlassIsDark + self.patternButtonNode.dark = controlsGlassIsDark + self.colorsButtonNode.dark = controlsGlassIsDark + var canShare = false var canSwitchTheme = false var canEdit = false @@ -948,14 +968,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { canSwitchTheme = true } - if canSwitchTheme { - self.dayNightButtonNode.isHidden = false - self.shareButtonNode.isHidden = true - } else { - self.dayNightButtonNode.isHidden = true - self.shareButtonNode.isHidden = !canShare - } - self.editButtonNode.isHidden = !canEdit + self.topCanSwitchTheme = canSwitchTheme + self.topCanShare = canShare + self.topCanEdit = canEdit if self.cropNode.supernode == nil { self.imageNode.contentMode = .scaleAspectFill @@ -1015,6 +1030,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { strongSelf.blurButtonNode.setEnabled(local) strongSelf.motionButtonNode.setEnabled(local) strongSelf.patternButtonNode.setEnabled(local) + strongSelf.setToolbarDoneEnabled(local) } })) @@ -1028,10 +1044,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } strongSelf.statusNode.backgroundNodeColor = color - strongSelf.patternButtonNode.buttonColor = color - strongSelf.blurButtonNode.buttonColor = color - strongSelf.motionButtonNode.buttonColor = color - strongSelf.colorsButtonNode.buttonColor = color })) } else if self.arguments.patternEnabled != previousArguments.patternEnabled { self.patternButtonNode.isSelected = self.arguments.patternEnabled @@ -1066,6 +1078,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: .immediate) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition:.immediate) } + if let (layout, toolbarHeight) = self.validToolbarLayout { + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: offset, y: 0.0), transition: .immediate) + } } func updateDismissTransition(_ value: CGFloat) { @@ -1073,6 +1088,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) } + if let (layout, toolbarHeight) = self.validToolbarLayout { + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: self.validOffset ?? 0.0, y: value), transition: .immediate) + } } var options: WallpaperPresentationOptions { @@ -1296,6 +1314,34 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } transition.updatePosition(node: self.wrapperNode, position: CGPoint(x: layout.size.width / 2.0 + appliedOffset, y: layout.size.height / 2.0)) } + + func setToolbarDoneEnabled(_ enabled: Bool) { + self.toolbarDoneEnabled = enabled + self.toolbarNode.setDoneEnabled(enabled) + } + + func setToolbarDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { + self.toolbarNode.setDoneIsSolid(isSolid, transition: transition) + } + + func updateToolbarThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.toolbarNode.updateThemeAndStrings(theme: theme, strings: strings) + self.toolbarNode.setDoneEnabled(self.toolbarDoneEnabled) + } + + func updateToolbarLayout(layout: ContainerViewLayout, toolbarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validToolbarLayout = (layout, toolbarHeight) + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: self.validOffset ?? 0.0, y: 0.0), transition: transition) + } + + private func updateToolbarLayout(layout: ContainerViewLayout, toolbarHeight: CGFloat, offset: CGPoint, transition: ContainedViewLayoutTransition) { + let toolbarFrame = CGRect( + origin: CGPoint(x: offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom + offset.y), + size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom) + ) + transition.updateFrame(node: self.toolbarNode, frame: toolbarFrame) + self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: toolbarHeight), layout: layout, transition: transition) + } func updateButtonsLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { let patternButtonSize = self.patternButtonNode.measure(layout.size) @@ -1328,10 +1374,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let buttonSpacing: CGFloat = 18.0 - var toolbarHeight: CGFloat = 66.0 - if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer { - toolbarHeight += 58.0 - } + let toolbarHeight = self.arguments.toolbarHeight let leftButtonFrame = CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0 - buttonSize.width - buttonSpacing) + offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) let centerButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0) + offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) @@ -1362,11 +1405,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } else { sliderFrame = sliderFrame.offsetBy(dx: 0.0, dy: 22.0) } - - let cancelSize = self.cancelButtonNode.measure(layout.size) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0 + offset.x, y: 16.0), size: cancelSize) - let shareFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - 28.0 + offset.x, y: 16.0), size: CGSize(width: 28.0, height: 28.0)) - let editFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - 28.0 + offset.x - 46.0, y: 16.0), size: CGSize(width: 28.0, height: 28.0)) + + var topCanShare = self.topCanShare let centerOffset: CGFloat = 32.0 @@ -1460,7 +1500,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { colorsAlpha = 0.0 blurAlpha = 0.0 playAlpha = 0.0 - self.shareButtonNode.isHidden = true + topCanShare = false } blurFrame = centerButtonFrame } @@ -1486,10 +1526,70 @@ final class WallpaperGalleryItemNode: GalleryItemNode { transition.updateTransformScale(node: self.sliderNode, scale: sliderScale) self.sliderNode.updateLayout(size: sliderFrame.size) - transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - transition.updateFrame(node: self.shareButtonNode, frame: shareFrame) - transition.updateFrame(node: self.dayNightButtonNode, frame: shareFrame) - transition.updateFrame(node: self.editButtonNode, frame: editFrame) + let leftControlItems: [GlassControlGroupComponent.Item] = [ + GlassControlGroupComponent.Item( + id: AnyHashable("close"), + content: .icon("Navigation/Close"), + action: { [weak self] in + self?.cancelPressed() + } + ) + ] + var rightControlItems: [GlassControlGroupComponent.Item] = [] + if self.topCanEdit { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("edit"), + content: .icon("Settings/WallpaperAdjustments"), + action: { [weak self] in + self?.editPressed() + } + )) + } + if self.topCanSwitchTheme { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("dayNight"), + content: .animation(self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun"), + action: { [weak self] in + self?.dayNightPressed() + } + )) + } else if topCanShare { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("share"), + content: .icon("Navigation/Share"), + action: { [weak self] in + self?.actionPressed() + } + )) + } + + let topButtonSideInset: CGFloat = 16.0 + let topButtonsSize = self.topButtons.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassControlPanelComponent( + theme: self.controlsGlassIsDark ? defaultDarkPresentationTheme : self.presentationData.theme, + leftItem: GlassControlPanelComponent.Item( + items: leftControlItems, + background: .panel + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: self.controlsGlassIsDark + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - topButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0) + ) + let topButtonsFrame = CGRect(origin: CGPoint(x: topButtonSideInset + layout.safeInsets.left + offset.x, y: topButtonSideInset), size: topButtonsSize) + if let topButtonsView = self.topButtons.view { + if topButtonsView.superview == nil { + self.buttonsContainerNode.view.addSubview(topButtonsView) + } + transition.updateFrame(view: topButtonsView, frame: topButtonsFrame) + } } private func updateMessagesLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { @@ -1750,6 +1850,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) + self.updateToolbarLayout(layout: layout, toolbarHeight: self.arguments.toolbarHeight, transition: transition) self.validLayout = (layout, navigationBarHeight) } @@ -1768,7 +1869,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } - let frame = self.dayNightButtonNode.view.convert(self.dayNightButtonNode.bounds, to: self.view) + guard let controlsView = self.topButtons.view as? GlassControlPanelComponent.View, let dayNightView = controlsView.rightItemView?.itemView(id: AnyHashable("dayNight")) else { + return + } + + let frame = dayNightView.convert(dayNightView.bounds, to: self.view) let currentTimestamp = Int32(Date().timeIntervalSince1970) let isDark = self.isDarkAppearance diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift index 0ce64b7b9c..2c1575bd40 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import ManagedAnimationNode import ComponentFlow import PremiumLockButtonSubtitleComponent +import ButtonComponent public enum WallpaperGalleryToolbarCancelButtonType { case cancel @@ -24,169 +25,129 @@ public enum WallpaperGalleryToolbarDoneButtonType { public protocol WallpaperGalleryToolbar: ASDisplayNode { var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { get set } var doneButtonType: WallpaperGalleryToolbarDoneButtonType { get set } - + var cancel: (() -> Void)? { get set } var done: ((Bool) -> Void)? { get set } - + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) - + func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) } public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { class ButtonNode: ASDisplayNode { private let strings: PresentationStrings - + private let doneButton = HighlightTrackingButtonNode() - private var doneButtonBackgroundNode: ASDisplayNode + private var doneButtonBackgroundNode: WallpaperOptionBackgroundNode private let doneButtonTitleNode: ImmediateTextNode private var doneButtonSubtitle: ComponentView? - + private let doneButtonSolidBackgroundNode: ASDisplayNode private let doneButtonSolidTitleNode: ImmediateTextNode - + private let animationNode: SimpleAnimationNode - + var action: () -> Void = {} - + var isLocked: Bool = false { didSet { self.animationNode.isHidden = !self.isLocked } } - + var requiredLevel: Int? - + init(strings: PresentationStrings) { self.strings = strings - - self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode() - self.doneButtonBackgroundNode.cornerRadius = 14.0 - + + self.doneButtonBackgroundNode = WallpaperOptionBackgroundNode(isDark: false) + self.doneButtonTitleNode = ImmediateTextNode() self.doneButtonTitleNode.displaysAsynchronously = false self.doneButtonTitleNode.isUserInteractionEnabled = false - + self.doneButtonSolidBackgroundNode = ASDisplayNode() self.doneButtonSolidBackgroundNode.alpha = 0.0 self.doneButtonSolidBackgroundNode.clipsToBounds = true - self.doneButtonSolidBackgroundNode.layer.cornerRadius = 14.0 + self.doneButtonSolidBackgroundNode.layer.cornerRadius = 26.0 if #available(iOS 13.0, *) { self.doneButtonSolidBackgroundNode.layer.cornerCurve = .continuous } self.doneButtonSolidBackgroundNode.isUserInteractionEnabled = false - + self.doneButtonSolidTitleNode = ImmediateTextNode() self.doneButtonSolidTitleNode.alpha = 0.0 self.doneButtonSolidTitleNode.displaysAsynchronously = false self.doneButtonSolidTitleNode.isUserInteractionEnabled = false - + self.animationNode = SimpleAnimationNode(animationName: "premium_unlock", size: CGSize(width: 30.0, height: 30.0)) self.animationNode.customColor = .white self.animationNode.isHidden = true - + super.init() - + self.doneButton.isExclusiveTouch = true self.addSubnode(self.doneButtonBackgroundNode) - self.addSubnode(self.doneButtonTitleNode) - - self.addSubnode(self.doneButtonSolidBackgroundNode) - self.addSubnode(self.doneButtonSolidTitleNode) - + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonTitleNode) + + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonSolidBackgroundNode) + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonSolidTitleNode) + self.addSubnode(self.animationNode) - - self.addSubnode(self.doneButton) - - self.doneButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - if strongSelf.isSolid { - strongSelf.doneButtonSolidBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSolidBackgroundNode.alpha = 0.55 - strongSelf.doneButtonSolidTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSolidTitleNode.alpha = 0.55 - } else { - strongSelf.doneButtonBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonBackgroundNode.alpha = 0.55 - strongSelf.doneButtonTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonTitleNode.alpha = 0.55 - - strongSelf.doneButtonSubtitle?.view?.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSubtitle?.view?.alpha = 0.55 - } - } else { - if strongSelf.isSolid { - strongSelf.doneButtonSolidBackgroundNode.alpha = 1.0 - strongSelf.doneButtonSolidBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - strongSelf.doneButtonSolidTitleNode.alpha = 1.0 - strongSelf.doneButtonSolidTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - } else { - strongSelf.doneButtonBackgroundNode.alpha = 1.0 - strongSelf.doneButtonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - strongSelf.doneButtonTitleNode.alpha = 1.0 - strongSelf.doneButtonTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - - strongSelf.doneButtonSubtitle?.view?.alpha = 1.0 - strongSelf.doneButtonSubtitle?.view?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - } - } - } - } - + + self.doneButtonBackgroundNode.contentView.addSubview(self.doneButton.view) + self.doneButton.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } - + func setEnabled(_ enabled: Bool) { self.doneButton.alpha = enabled ? 1.0 : 0.4 self.doneButton.isUserInteractionEnabled = enabled } - + private var isSolid = false func setIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { guard self.isSolid != isSolid else { return } self.isSolid = isSolid - + transition.updateAlpha(node: self.doneButtonBackgroundNode, alpha: isSolid ? 0.0 : 1.0) transition.updateAlpha(node: self.doneButtonSolidBackgroundNode, alpha: isSolid ? 1.0 : 0.0) transition.updateAlpha(node: self.doneButtonTitleNode, alpha: isSolid ? 0.0 : 1.0) transition.updateAlpha(node: self.doneButtonSolidTitleNode, alpha: isSolid ? 1.0 : 0.0) } - + func updateTitle(_ title: String, theme: PresentationTheme) { - self.doneButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .white) - + self.doneButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.dark ? .white : .black) + self.doneButtonSolidBackgroundNode.backgroundColor = theme.list.itemCheckColors.fillColor self.doneButtonSolidTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor) } - + func updateSize(_ size: CGSize) { let bounds = CGRect(origin: .zero, size: size) self.doneButtonBackgroundNode.frame = bounds - if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.updateLayout(size: size) - } else if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperLightButtonBackgroundNode { - backgroundNode.updateLayout(size: size) - } + self.doneButtonBackgroundNode.updateLayout(size: size) self.doneButtonSolidBackgroundNode.frame = bounds - + self.doneButtonSolidBackgroundNode.layer.cornerRadius = size.height * 0.5 + let constrainedSize = CGSize(width: size.width - 44.0, height: size.height) let iconSize = CGSize(width: 30.0, height: 30.0) let doneTitleSize = self.doneButtonTitleNode.updateLayout(constrainedSize) - + var totalWidth = doneTitleSize.width if self.isLocked { totalWidth += iconSize.width + 1.0 } let titleOriginX = floorToScreenPixels((bounds.width - totalWidth) / 2.0) - + self.animationNode.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels((bounds.height - iconSize.height) / 2.0)), size: iconSize) - + var titleFrame = CGRect(origin: CGPoint(x: titleOriginX + totalWidth - doneTitleSize.width, y: floorToScreenPixels((bounds.height - doneTitleSize.height) / 2.0)), size: doneTitleSize).offsetBy(dx: bounds.minX, dy: bounds.minY) - + if let requiredLevel = self.requiredLevel { let subtitle: ComponentView if let current = self.doneButtonSubtitle { @@ -195,7 +156,7 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT subtitle = ComponentView() self.doneButtonSubtitle = subtitle } - + let subtitleSize = subtitle.update( transition: .immediate, component: AnyComponent( @@ -208,43 +169,36 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT environment: {}, containerSize: size ) - + if let view = subtitle.view { if view.superview == nil { view.isUserInteractionEnabled = false self.view.addSubview(view) } - + titleFrame.origin.y -= 8.0 - + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 3.0), size: subtitleSize) view.frame = subtitleFrame } } - + self.doneButtonTitleNode.frame = titleFrame - + let _ = self.doneButtonSolidTitleNode.updateLayout(constrainedSize) self.doneButtonSolidTitleNode.frame = self.doneButtonTitleNode.frame - + self.doneButton.frame = bounds } - + var dark: Bool = false { didSet { if self.dark != oldValue { - self.doneButtonBackgroundNode.removeFromSupernode() - if self.dark { - self.doneButtonBackgroundNode = WallpaperOptionBackgroundNode(enableSaturation: true) - } else { - self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode() - } - self.doneButtonBackgroundNode.cornerRadius = 14.0 - self.insertSubnode(self.doneButtonBackgroundNode, at: 0) + self.doneButtonBackgroundNode.isDark = self.dark } } } - + private var previousActionTime: Double? @objc func pressed() { let currentTime = CACurrentMediaTime() @@ -255,10 +209,10 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT self.action() } } - + private var theme: PresentationTheme private let strings: PresentationStrings - + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) @@ -269,44 +223,42 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - + public var dark: Bool = false { didSet { self.applyButton.dark = self.dark self.applyForBothButton.dark = self.dark } } - + private let applyButton: ButtonNode private let applyForBothButton: ButtonNode - + public var cancel: (() -> Void)? public var done: ((Bool) -> Void)? - + var requiredLevel: Int? { didSet { self.applyButton.requiredLevel = self.requiredLevel } } - + public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme self.strings = strings self.cancelButtonType = cancelButtonType self.doneButtonType = doneButtonType - + self.applyButton = ButtonNode(strings: strings) self.applyForBothButton = ButtonNode(strings: strings) - + super.init() - + self.addSubnode(self.applyButton) - if case .setPeer = doneButtonType { - self.addSubnode(self.applyForBothButton) - } - + self.addSubnode(self.applyForBothButton) + self.updateThemeAndStrings(theme: theme, strings: strings) - + self.applyButton.action = { [weak self] in if let self { self.done?(false) @@ -318,26 +270,27 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT } } } - + public func setDoneEnabled(_ enabled: Bool) { self.applyButton.setEnabled(enabled) self.applyForBothButton.setEnabled(enabled) } - + private var isSolid = false public func setDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { guard self.isSolid != isSolid else { return } - + self.isSolid = isSolid self.applyButton.setIsSolid(isSolid, transition: transition) self.applyForBothButton.setIsSolid(isSolid, transition: transition) } - + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - + self.applyButton.isUserInteractionEnabled = true + let applyTitle: String var applyForBothTitle: String? = nil var applyForBothLocked = false @@ -358,35 +311,36 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT applyTitle = "" self.applyButton.isUserInteractionEnabled = false } - + self.applyButton.updateTitle(applyTitle, theme: theme) if let applyForBothTitle { self.applyForBothButton.updateTitle(applyForBothTitle, theme: theme) } self.applyForBothButton.isLocked = applyForBothLocked } - + public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - let inset: CGFloat = 16.0 - let buttonHeight: CGFloat = 50.0 - + let inset: CGFloat = 30.0 + let buttonHeight: CGFloat = 52.0 + let spacing: CGFloat = 8.0 - + let applyFrame = CGRect(origin: CGPoint(x: inset, y: 2.0), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight)) let applyForBothFrame = CGRect(origin: CGPoint(x: inset, y: applyFrame.maxY + spacing), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight)) - + var showApplyForBothButton = false if case .setPeer = self.doneButtonType { showApplyForBothButton = true } transition.updateAlpha(node: self.applyForBothButton, alpha: showApplyForBothButton ? 1.0 : 0.0) - + self.applyForBothButton.isUserInteractionEnabled = showApplyForBothButton + self.applyButton.frame = applyFrame self.applyButton.updateSize(applyFrame.size) self.applyForBothButton.frame = applyForBothFrame self.applyForBothButton.updateSize(applyForBothFrame.size) } - + @objc func cancelPressed() { self.cancel?() } @@ -395,7 +349,7 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { private var theme: PresentationTheme private let strings: PresentationStrings - + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) @@ -406,119 +360,135 @@ public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalle self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - - private let cancelButton = HighlightTrackingButtonNode() - private let cancelHighlightBackgroundNode = ASDisplayNode() - private let doneButton = HighlightTrackingButtonNode() - private let doneHighlightBackgroundNode = ASDisplayNode() - private let backgroundNode = NavigationBackgroundNode(color: .clear) - private let separatorNode = ASDisplayNode() - private let topSeparatorNode = ASDisplayNode() - + + private let cancelButton = ComponentView() + private let doneButton = ComponentView() + + private var cancelTitle: String = "" + private var doneTitle: String = "" + private var doneEnabled: Bool = true + private var validLayout: (CGSize, ContainerViewLayout)? + public var cancel: (() -> Void)? public var done: ((Bool) -> Void)? - + public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme self.strings = strings self.cancelButtonType = cancelButtonType self.doneButtonType = doneButtonType - - self.cancelHighlightBackgroundNode.alpha = 0.0 - self.doneHighlightBackgroundNode.alpha = 0.0 - + super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.cancelHighlightBackgroundNode) - self.addSubnode(self.cancelButton) - self.addSubnode(self.doneHighlightBackgroundNode) - self.addSubnode(self.doneButton) - self.addSubnode(self.separatorNode) - self.addSubnode(self.topSeparatorNode) - self.updateThemeAndStrings(theme: theme, strings: strings) - - self.cancelButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.cancelHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.cancelHighlightBackgroundNode.alpha = 1.0 - } else { - strongSelf.cancelHighlightBackgroundNode.alpha = 0.0 - strongSelf.cancelHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.doneButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.doneHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneHighlightBackgroundNode.alpha = 1.0 - } else { - strongSelf.doneHighlightBackgroundNode.alpha = 0.0 - strongSelf.doneHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) } - + public func setDoneEnabled(_ enabled: Bool) { - self.doneButton.alpha = enabled ? 1.0 : 0.4 - self.doneButton.isUserInteractionEnabled = enabled + self.doneEnabled = enabled + if let (size, layout) = self.validLayout { + self.updateLayout(size: size, layout: layout, transition: .immediate) + } } - + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate) - self.separatorNode.backgroundColor = theme.rootController.tabBar.separatorColor - self.topSeparatorNode.backgroundColor = theme.rootController.tabBar.separatorColor - self.cancelHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor - self.doneHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor - - let cancelTitle: String + switch self.cancelButtonType { case .cancel: - cancelTitle = strings.Common_Cancel + self.cancelTitle = strings.Common_Cancel case .discard: - cancelTitle = strings.WallpaperPreview_PatternPaternDiscard + self.cancelTitle = strings.WallpaperPreview_PatternPaternDiscard } - let doneTitle: String switch self.doneButtonType { case .set, .setPeer, .setChannel: - doneTitle = strings.Wallpaper_Set + self.doneTitle = strings.Wallpaper_Set case .proceed: - doneTitle = strings.Theme_Colors_Proceed + self.doneTitle = strings.Theme_Colors_Proceed case .apply: - doneTitle = strings.WallpaperPreview_PatternPaternApply + self.doneTitle = strings.WallpaperPreview_PatternPaternApply case .none: - doneTitle = "" - self.doneButton.isUserInteractionEnabled = false + self.doneTitle = "" + } + + if let (size, layout) = self.validLayout { + self.updateLayout(size: size, layout: layout, transition: .immediate) } - self.cancelButton.setTitle(cancelTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) - self.doneButton.setTitle(doneTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) } - + public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.cancelButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) - self.cancelHighlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) - self.doneButton.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) - self.doneHighlightBackgroundNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) - self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height + layout.intrinsicInsets.bottom)) - self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)) - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.backgroundNode.update(size: CGSize(width: size.width, height: size.height + layout.intrinsicInsets.bottom), transition: .immediate) - } - - @objc func cancelPressed() { - self.cancel?() - } - - @objc func donePressed() { - self.done?(false) + self.validLayout = (size, layout) + + let sideInset: CGFloat = 30.0 + let spacing: CGFloat = 24.0 + let buttonHeight: CGFloat = 52.0 + let buttonWidth = floor(max(1.0, size.width - sideInset * 2.0 - spacing) / 2.0) + let buttonY = floor((size.height - buttonHeight) / 2.0) + let cancelFrame = CGRect(x: sideInset, y: buttonY, width: buttonWidth, height: buttonHeight) + let doneFrame = CGRect(x: cancelFrame.maxX + spacing, y: buttonY, width: max(1.0, size.width - sideInset - cancelFrame.maxX - spacing), height: buttonHeight) + + let glassColor = UIColor(white: 1.0, alpha: 0.0) + let foregroundColor = self.theme.list.itemPrimaryTextColor + let componentTransition = ComponentTransition(transition) + + let cancelSize = self.cancelButton.update( + transition: componentTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .actualGlass, + color: glassColor, + foreground: foregroundColor, + pressedColor: glassColor, + cornerRadius: buttonHeight * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(self.cancelTitle), + component: AnyComponent(Text(text: self.cancelTitle, font: Font.semibold(17.0), color: foregroundColor)) + ), + action: { [weak self] in + self?.cancel?() + } + )), + environment: {}, + containerSize: cancelFrame.size + ) + if let cancelView = self.cancelButton.view { + if cancelView.superview == nil { + self.view.addSubview(cancelView) + } + transition.updateFrame(view: cancelView, frame: CGRect(origin: cancelFrame.origin, size: cancelSize)) + } + + let doneIsVisible = !self.doneTitle.isEmpty + let doneSize = self.doneButton.update( + transition: componentTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .actualGlass, + color: glassColor, + foreground: foregroundColor, + pressedColor: glassColor, + cornerRadius: buttonHeight * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(self.doneTitle), + component: AnyComponent(Text(text: self.doneTitle, font: Font.semibold(17.0), color: foregroundColor)) + ), + isEnabled: self.doneEnabled && doneIsVisible, + tintWhenDisabled: false, + action: { [weak self] in + self?.done?(false) + } + )), + environment: {}, + containerSize: doneFrame.size + ) + if let doneView = self.doneButton.view { + if doneView.superview == nil { + self.view.addSubview(doneView) + } + transition.updateFrame(view: doneView, frame: CGRect(origin: doneFrame.origin, size: doneSize)) + componentTransition.setAlpha(view: doneView, alpha: doneIsVisible ? (self.doneEnabled ? 1.0 : 0.4) : 0.0) + doneView.isUserInteractionEnabled = self.doneEnabled && doneIsVisible + } } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift index 4129277be3..2b9aa4b32a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift @@ -5,6 +5,8 @@ import AsyncDisplayKit import SwiftSignalKit import CheckNode import AnimationUI +import ComponentFlow +import GlassBackgroundComponent public enum WallpaperOptionButtonValue { case check(Bool) @@ -68,31 +70,43 @@ final class WallpaperLightButtonBackgroundNode: ASDisplayNode { } final class WallpaperOptionBackgroundNode: ASDisplayNode { - private let backgroundNode: NavigationBackgroundNode - var enableSaturation: Bool { didSet { - self.backgroundNode.updateColor(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: self.enableSaturation, transition: .immediate) } } - init(enableSaturation: Bool = false) { + var isDark: Bool { + didSet { + if self.isDark != oldValue, let validSize = self.validSize { + self.updateLayout(size: validSize) + } + } + } + + private var validSize: CGSize? + + init(enableSaturation: Bool = false, isDark: Bool = true) { self.enableSaturation = enableSaturation - self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: enableSaturation) + self.isDark = isDark super.init() - - self.clipsToBounds = true - self.isUserInteractionEnabled = false - - self.addSubnode(self.backgroundNode) + + self.setViewBlock({ + return GlassBackgroundView() + }) + } + + private var glassView: GlassBackgroundView { + return self.view as! GlassBackgroundView } - func updateLayout(size: CGSize) { - let frame = CGRect(origin: .zero, size: size) - self.backgroundNode.frame = frame - - self.backgroundNode.update(size: size, transition: .immediate) + var contentView: UIView { + return self.glassView.contentView + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + self.validSize = size + self.glassView.update(size: size, cornerRadius: size.height * 0.5, isDark: self.isDark, tintColor: .init(kind: .panel), isInteractive: true, transition: ComponentTransition(transition)) } } @@ -105,9 +119,7 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { var enableSaturation: Bool = false { didSet { - if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.enableSaturation = self.enableSaturation - } + self.backgroundNode.enableSaturation = self.enableSaturation } } @@ -117,105 +129,70 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { if self.dark != oldValue { self.backgroundNode.removeFromSupernode() if self.dark { - self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: true) } else { - self.backgroundNode = WallpaperLightButtonBackgroundNode() + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: false) } self.insertSubnode(self.backgroundNode, at: 0) } } } - private var backgroundNode: ASDisplayNode + private var backgroundNode: WallpaperOptionBackgroundNode private let iconNode: ASImageNode private let textNode: ImmediateTextNode private var animationNode: AnimationNode? func setIcon(_ image: UIImage?) { - self.iconNode.image = generateTintedImage(image: image, color: .white) + self.iconNode.image = generateTintedImage(image: image, color: self.dark ? .white : .black) } init(content: Content, dark: Bool) { self.content = content self.dark = dark - if dark { - self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) - } else { - self.backgroundNode = WallpaperLightButtonBackgroundNode() - } + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: dark) self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.contentMode = .center + let iconColor: UIColor = dark ? .white : .black + var title: String switch content { case let .text(text): title = text case let .icon(icon, _): title = "" - self.iconNode.image = generateTintedImage(image: icon, color: .white) + self.iconNode.image = generateTintedImage(image: icon, color: .black) case let .dayNight(isNight): title = "" - let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: [:], scale: 1.0) + let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: ["__allcolors__": iconColor], scale: 1.0) animationNode.speed = 1.66 animationNode.isUserInteractionEnabled = false self.animationNode = animationNode } self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white) + self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: iconColor) super.init() self.isExclusiveTouch = true self.addSubnode(self.backgroundNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.textNode) + self.backgroundNode.contentView.addSubnode(self.iconNode) + self.backgroundNode.contentView.addSubnode(self.textNode) if let animationNode = self.animationNode { - self.addSubnode(animationNode) - } - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 0.4 - - strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.iconNode.alpha = 0.4 - - strongSelf.textNode.layer.removeAnimation(forKey: "opacity") - strongSelf.textNode.alpha = 0.4 - -// if let animationNode = strongSelf.animationNode { -// animationNode.layer.removeAnimation(forKey: "opacity") -// animationNode.alpha = 0.4 -// } - } else { - strongSelf.backgroundNode.alpha = 1.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.iconNode.alpha = 1.0 - strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.textNode.alpha = 1.0 - strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - -// if let animationNode = strongSelf.animationNode { -// animationNode.alpha = 1.0 -// animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) -// } - } - } + self.backgroundNode.contentView.addSubnode(animationNode) } } func setIsNight(_ isNight: Bool) { - self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: [:]) + let iconColor: UIColor = self.dark ? .white : .black + self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: ["__allcolors__": iconColor]) self.animationNode?.speed = 1.66 self.animationNode?.playOnce() @@ -225,11 +202,6 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { } } - var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { - didSet { - } - } - private var textSize: CGSize? override func measure(_ constrainedSize: CGSize) -> CGSize { switch self.content { @@ -249,11 +221,7 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { let size = self.bounds.size self.backgroundNode.frame = self.bounds - if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) - } else if let backgroundNode = self.backgroundNode as? WallpaperLightButtonBackgroundNode { - backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) - } + self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) self.backgroundNode.cornerRadius = size.height / 2.0 self.iconNode.frame = self.bounds @@ -303,7 +271,16 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { public var title: String { didSet { - self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white) + self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: self.dark ? .white : .black) + } + } + + public var dark: Bool = false { + didSet { + let color: UIColor = self.dark ? .white : .black + self.checkNode.theme = CheckNodeTheme(backgroundColor: color, strokeColor: .clear, borderColor: color, overlayBorder: false, hasInset: false, hasShadow: false, borderWidth: 1.5) + self.textNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(13), textColor: self.dark ? .white : .black) + let _ = self.textNode.updateLayout(CGSize(width: 160.0, height: 40.0)) } } @@ -324,8 +301,6 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { super.init() - self.clipsToBounds = true - self.cornerRadius = 14.0 self.isExclusiveTouch = true switch value { @@ -345,32 +320,9 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { self.addSubnode(self.backgroundNode) - self.addSubnode(self.checkNode) - self.addSubnode(self.textNode) - self.addSubnode(self.colorNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 0.4 - - strongSelf.colorNode.layer.removeAnimation(forKey: "opacity") - strongSelf.colorNode.alpha = 0.4 - } else { - strongSelf.backgroundNode.alpha = 1.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.colorNode.alpha = 1.0 - strongSelf.colorNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - public var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { - didSet { - } + self.backgroundNode.contentView.addSubnode(self.checkNode) + self.backgroundNode.contentView.addSubnode(self.textNode) + self.backgroundNode.contentView.addSubnode(self.colorNode) } public var color: UIColor? { diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift index bed529da7e..73238caf47 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift @@ -293,6 +293,7 @@ public final class WallpaperPatternPanelNode: ASDisplayNode { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.alwaysBounceHorizontal = true + self.scrollNode.view.scrollsToTop = false let sliderView = TGPhotoEditorSliderView() sliderView.disableSnapToPositions = true diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift index 242cffb640..c6ff0f4561 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift @@ -49,12 +49,16 @@ final class ThemeColorsGridControllerItemNode: GridItemNode { self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) super.init() + self.clipsToBounds = true + self.addSubnode(self.wallpaperNode) } override func didLoad() { super.didLoad() + self.view.layer.cornerRadius = 16.0 + self.view.isExclusiveTouch = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift index 315d529ec3..4bed625ea8 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift @@ -74,7 +74,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { let ready = ValuePromise() private var topBackgroundNode: ASDisplayNode - private var separatorNode: ASDisplayNode + private let maskNode: ASImageNode private let customColorItemNode: ItemListActionItemNode private var customColorItem: ItemListActionItem @@ -105,8 +105,8 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.topBackgroundNode = ASDisplayNode() self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false self.customColorItemNode = ItemListActionItemNode() self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { @@ -124,12 +124,13 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { if case .default = controller.mode { self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.gridNode.addSubnode(self.topBackgroundNode) - self.gridNode.addSubnode(self.separatorNode) self.gridNode.addSubnode(self.customColorItemNode) } else { self.backgroundColor = presentationData.theme.list.plainBackgroundColor } self.addSubnode(self.gridNode) + self.gridNode.addSubnode(self.maskNode) + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) let previousEntries = Atomic<[ThemeColorsGridControllerEntry]?>(value: nil) @@ -227,6 +228,27 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { } } self.gridNode.view.addGestureRecognizer(tapRecognizer) + + self.gridNode.presentationLayoutUpdated = { [weak self] gridLayout, transition in + if let strongSelf = self, let (layout, _) = strongSelf.validLayout { + let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + let maskSideInset: CGFloat = layout.size.width >= 320.0 ? sideInset : 0.0 + + let maskY: CGFloat + if let controller = strongSelf.controller, case .default = controller.mode { + let buttonTopInset: CGFloat = 32.0 + let buttonHeight: CGFloat = 44.0 + let buttonBottomInset: CGFloat = 35.0 + let buttonInset = buttonTopInset + buttonHeight + buttonBottomInset + let buttonOffset = buttonInset + 10.0 + maskY = -buttonOffset + buttonInset + } else { + maskY = 0.0 + } + + transition.updateFrame(node: strongSelf.maskNode, frame: CGRect(origin: CGPoint(x: maskSideInset, y: maskY), size: CGSize(width: layout.size.width - maskSideInset * 2.0, height: gridLayout.contentSize.height + 10.0))) + } + } } @@ -263,7 +285,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor - self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentColorPicker() @@ -297,6 +319,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { return } let hadValidLayout = self.validLayout != nil + self.validLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -304,42 +327,23 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { insets.right = layout.safeInsets.right let scrollIndicatorInsets = insets - let itemsPerRow: Int - if case .compact = layout.metrics.widthClass { - switch layout.orientation { - case .portrait: - itemsPerRow = 3 - case .landscape: - itemsPerRow = 5 - } - } else { - itemsPerRow = 3 - } + let padding: CGFloat = 12.0 + let minSpacing: CGFloat = 6.0 let referenceImageSize: CGSize let screenWidth = min(layout.size.width, layout.size.height) - if screenWidth >= 375.0 { + if screenWidth >= 390.0 { referenceImageSize = CGSize(width: 108.0, height: 108.0) } else { referenceImageSize = CGSize(width: 91.0, height: 91.0) } - let width = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - let imageSize: CGSize - let spacing: CGFloat - var fillWidth: Bool? - if case .peer = controller.mode { - spacing = 1.0 - - let itemWidth = floorToScreenPixels((width - spacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) - imageSize = CGSize(width: itemWidth, height: itemWidth) - fillWidth = true - } else { - let minSpacing = 8.0 - - imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((width - CGFloat(itemsPerRow + 1) * minSpacing) / CGFloat(itemsPerRow)), height: referenceImageSize.height)) - spacing = floor((width - CGFloat(itemsPerRow) * imageSize.width) / CGFloat(itemsPerRow + 1)) - } + let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + let gridWidth = layout.size.width - sideInset * 2.0 + let imageCount = max(2, Int((gridWidth - padding * 2.0) / referenceImageSize.width)) + let itemWidth = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)) + let imageSize = CGSize(width: itemWidth, height: itemWidth) + let spacing = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) let buttonTopInset: CGFloat = 32.0 let buttonHeight: CGFloat = 44.0 @@ -349,26 +353,27 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { var buttonOffset = buttonInset + 10.0 var listInsets = insets - if case .default = controller.mode { - if layout.size.width >= 375.0 { - let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - listInsets.left += inset - listInsets.right += inset - - if self.leftOverlayNode.supernode == nil { - self.gridNode.addSubnode(self.leftOverlayNode) - } - if self.rightOverlayNode.supernode == nil { - self.gridNode.addSubnode(self.rightOverlayNode) - } - } else { - if self.leftOverlayNode.supernode != nil { - self.leftOverlayNode.removeFromSupernode() - } - if self.rightOverlayNode.supernode != nil { - self.rightOverlayNode.removeFromSupernode() - } + if layout.size.width >= 320.0 { + listInsets.left = sideInset + listInsets.right = sideInset + + if self.leftOverlayNode.supernode == nil { + self.gridNode.addSubnode(self.leftOverlayNode) } + if self.rightOverlayNode.supernode == nil { + self.gridNode.addSubnode(self.rightOverlayNode) + } + } else { + if self.leftOverlayNode.supernode != nil { + self.leftOverlayNode.removeFromSupernode() + } + if self.rightOverlayNode.supernode != nil { + self.rightOverlayNode.removeFromSupernode() + } + } + + if case .default = controller.mode { + self.customColorItemNode.isHidden = false } else { self.customColorItemNode.isHidden = true buttonOffset = 0.0 @@ -381,19 +386,19 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { colorApply(false) transition.updateFrame(node: self.topBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.customColorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset), size: colorLayout.contentSize)) - self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel) - self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel) + self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + 10000.0) + self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + 10000.0) insets.top += spacing + buttonInset - - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: fillWidth, lineSpacing: spacing, itemSpacing: fillWidth != nil ? spacing : nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + listInsets.top = insets.top + listInsets.left += 3.0 + listInsets.right += 3.0 self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: listInsets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - self.validLayout = (layout, navigationBarHeight) if !hadValidLayout { self.dequeueTransitions() } @@ -406,7 +411,6 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.topBackgroundNode.layer.animatePosition(from: self.topBackgroundNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.topBackgroundNode.layer.position, duration: duration) - self.separatorNode.layer.animatePosition(from: self.separatorNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.separatorNode.layer.position, duration: duration) self.customColorItemNode.layer.animatePosition(from: self.customColorItemNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.customColorItemNode.layer.position, duration: duration) } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index 2c4d0213fa..da424a986a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -115,7 +115,7 @@ public final class ThemeGridController: ViewController { if let isEmpty = self.isEmpty, isEmpty { } else { if self.editingMode { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -478,7 +478,7 @@ public final class ThemeGridController: ViewController { @objc func editPressed() { self.editingMode = true - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.controllerNode.updateState { state in var state = state state.editing = true diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift index 13f0696dca..6cd5b48ced 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift @@ -15,7 +15,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec if let image = cachedBorderImages[key] { return image } else { - let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -41,7 +41,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) } - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) cachedBorderImages[key] = image return image } @@ -113,7 +113,7 @@ final class ThemeGridControllerItemNode: GridItemNode { override func didLoad() { super.didLoad() - self.view.layer.cornerRadius = 10.0 + self.view.layer.cornerRadius = 16.0 self.view.isExclusiveTouch = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift index 7c2b63a292..4ca22beb39 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift @@ -320,7 +320,7 @@ final class ThemeGridControllerNode: ASDisplayNode { self.addSubnode(self.gridNode) self.gridNode.addSubnode(self.maskNode) - self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true) + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil) let interaction = ThemeGridControllerInteraction(openWallpaper: { [weak self] wallpaper in @@ -577,7 +577,7 @@ final class ThemeGridControllerNode: ASDisplayNode { let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var listInsets = layout.safeInsets - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { listInsets.left = sideInset listInsets.right = sideInset } @@ -596,7 +596,7 @@ final class ThemeGridControllerNode: ASDisplayNode { transition.updateFrame(node: strongSelf.resetDescriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: gridLayout.contentSize.height + 35.0 + resetLayout.contentSize.height), size: resetDescriptionLayout.contentSize)) let maskSideInset = strongSelf.leftOverlayNode.frame.maxX - strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: maskSideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel + 4.0), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 6.0)) + strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: maskSideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 10.0)) } } } @@ -766,22 +766,23 @@ final class ThemeGridControllerNode: ASDisplayNode { var scrollIndicatorInsets = insets + let padding: CGFloat = 12.0 let minSpacing: CGFloat = 6.0 + let referenceImageSize: CGSize let screenWidth = min(layout.size.width, layout.size.height) if screenWidth >= 390.0 { - referenceImageSize = CGSize(width: 112.0, height: 150.0) + referenceImageSize = CGSize(width: 110.0, height: 150.0) } else { - referenceImageSize = CGSize(width: 91.0, height: 161.0) + referenceImageSize = CGSize(width: 90.0, height: 150.0) } - let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) let gridWidth = layout.size.width - sideInset * 2.0 - let imageCount = Int((gridWidth - minSpacing * 2.0) / (referenceImageSize.width)) - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((gridWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) - let spacing = floor((gridWidth - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) + let imageCount = Int((gridWidth - padding * 2.0) / (referenceImageSize.width)) + let imageSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + let spacing = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) let makeColorLayout = self.colorItemNode.asyncLayout() let makeGalleryLayout = (self.galleryItemNode as? ItemListActionItemNode)?.asyncLayout() @@ -790,7 +791,7 @@ final class ThemeGridControllerNode: ASDisplayNode { let makeDescriptionLayout = self.descriptionItemNode.asyncLayout() var listInsets = insets - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { listInsets.left = sideInset listInsets.right = sideInset if self.leftOverlayNode.supernode == nil { @@ -850,10 +851,10 @@ final class ThemeGridControllerNode: ASDisplayNode { } let buttonOffset = buttonInset + 10.0 - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 504.0))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - var originY = -buttonOffset + buttonTopInset + var originY = -buttonOffset + buttonTopInset - 4.0 if !isChannel { transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: colorLayout.contentSize)) originY += colorLayout.contentSize.height @@ -876,7 +877,9 @@ final class ThemeGridControllerNode: ASDisplayNode { self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height + 10000.0) insets.top += spacing + buttonInset - listInsets.top = insets.top + listInsets.top = insets.top + 4.0 + listInsets.left += 3.0 + listInsets.right += 3.0 if self.currentState.editing { let panelHeight: CGFloat diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift deleted file mode 100644 index e3d5a34364..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift +++ /dev/null @@ -1,257 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ListSectionHeaderNode - -private class ThemeGridColorNode: HighlightableButtonNode { - let action: () -> Void - - init(color: WallpaperSearchColor, strokeColor: UIColor, dark: Bool, action: @escaping (WallpaperSearchColor) -> Void) { - self.action = { - action(color) - } - - super.init() - - let image: UIImage? - if color == .white && !dark { - image = generateFilledCircleImage(diameter: 42.0, color: .white, strokeColor: strokeColor, strokeWidth: 1.0) - } else if color == .black && dark { - image = generateFilledCircleImage(diameter: 42.0, color: .black, strokeColor: strokeColor, strokeWidth: 1.0) - } else { - image = generateFilledCircleImage(diameter: 42.0, color: color.displayColor) - } - self.setImage(image, for: .normal) - } - - override func didLoad() { - super.didLoad() - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - } - - @objc func pressed() { - self.action() - } -} - -private let inset: CGFloat = 15.0 -private let diameter: CGFloat = 42.0 - -final class ThemeGridSearchColorsNode: ASDisplayNode { - private var theme: PresentationTheme - private var strings: PresentationStrings - private let sectionHeaderNode: ListSectionHeaderNode - private let scrollNode: ASScrollNode - - private let colorSelected: (WallpaperSearchColor) -> Void - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) { - self.theme = theme - self.strings = strings - - self.colorSelected = colorSelected - - self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) - self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased() - - self.scrollNode = ASScrollNode() - self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.showsVerticalScrollIndicator = false - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - - super.init() - - self.addSubnode(self.sectionHeaderNode) - self.addSubnode(self.scrollNode) - - self.scrollNode.view.contentSize = CGSize(width: (inset + diameter) * CGFloat(WallpaperSearchColor.allCases.count) + inset, height: 71.0) - - for color in WallpaperSearchColor.allCases { - let colorNode = ThemeGridColorNode(color: color, strokeColor: theme.list.controlSecondaryColor, dark: theme.overallDarkAppearance, action: colorSelected) - self.scrollNode.addSubnode(colorNode) - } - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if self.theme !== theme || self.strings !== strings { - self.theme = theme - self.strings = strings - - self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased() - self.sectionHeaderNode.updateTheme(theme: theme) - } - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 100.0) - } - - private var validLayout: (CGSize, CGFloat, CGFloat)? - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { - let hadLayout = self.validLayout != nil - self.validLayout = (size, leftInset, rightInset) - - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 29.0)) - self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 29.0), leftInset: leftInset, rightInset: rightInset) - - var insets = UIEdgeInsets() - insets.left += leftInset - insets.right += rightInset - - self.scrollNode.frame = CGRect(x: 0.0, y: 29.0, width: size.width, height: size.height - 29.0) - self.scrollNode.view.contentInset = insets - if !hadLayout { - self.scrollNode.view.contentOffset = CGPoint(x: -leftInset, y: 0.0) - } - - var offset: CGFloat = inset - if let subnodes = self.scrollNode.subnodes { - for node in subnodes { - node.frame = CGRect(x: offset, y: inset, width: diameter, height: diameter) - offset += diameter + inset - } - } - } -} - - -class ThemeGridSearchColorsItem: ListViewItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings - let colorSelected: (WallpaperSearchColor) -> Void - - let header: ListViewItemHeader? - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) { - self.account = account - self.theme = theme - self.strings = strings - self.colorSelected = colorSelected - self.header = nil - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = ThemeGridSearchColorsItemNode() - let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem != nil) - node.contentSize = nodeLayout.contentSize - node.insets = nodeLayout.insets - - completion(node, nodeApply) - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? ThemeGridSearchColorsItemNode { - let layout = nodeValue.asyncLayout() - async { - let (nodeLayout, apply) = layout(self, params, nextItem != nil) - Queue.mainQueue().async { - completion(nodeLayout, { info in - apply().1(info) - }) - } - } - } - } - } -} - -class ThemeGridSearchColorsItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private var colorsNode: ThemeGridSearchColorsNode? - - private var item: ThemeGridSearchColorsItem? - - required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - super.init(layerBacked: false) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = self.item { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - let _ = nodeApply() - } - } - - func asyncLayout() -> (_ item: ThemeGridSearchColorsItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { - let currentItem = self.item - - return { [weak self] item, params, last in - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 101.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - - return (nodeLayout, { [weak self] in - var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - return (nil, { _ in - if let strongSelf = self { - strongSelf.item = item - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - } - - let colorsNode: ThemeGridSearchColorsNode - if let currentColorsNode = strongSelf.colorsNode { - colorsNode = currentColorsNode - colorsNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) - } else { - colorsNode = ThemeGridSearchColorsNode(account: item.account, theme: item.theme, strings: item.strings, colorSelected: item.colorSelected) - strongSelf.colorsNode = colorsNode - strongSelf.addSubnode(colorsNode) - } - - colorsNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) - colorsNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - let separatorHeight = UIScreenPixel - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) - strongSelf.separatorNode.isHidden = true - } - }) - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } - - override public func headers() -> [ListViewItemHeader]? { - if let item = self.item { - return item.header.flatMap { [$0] } - } else { - return nil - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift deleted file mode 100644 index 19b3e8a5c2..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift +++ /dev/null @@ -1,833 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import MergeLists -import AccountContext -import SearchUI -import ChatListSearchItemHeader -import WebSearchUI -import SearchBarNode - -enum WallpaperSearchColor: CaseIterable { - case blue - case red - case orange - case yellow - case green - case teal - case purple - case pink - case brown - case black - case gray - case white - - var string: String { - switch self { - case .blue: - return "Blue" - case .red: - return "Red" - case .orange: - return "Orange" - case .yellow: - return "Yellow" - case .green: - return "Green" - case .teal: - return "Teal" - case .purple: - return "Purple" - case .pink: - return "Pink" - case .brown: - return "Brown" - case .black: - return "Black" - case .gray: - return "Gray" - case .white: - return "White" - } - } - - var displayColor: UIColor { - switch self { - case .blue: - return UIColor(rgb: 0x0076ff) - case .red: - return UIColor(rgb: 0xff0000) - case .orange: - return UIColor(rgb: 0xff8a00) - case .yellow: - return UIColor(rgb: 0xffca00) - case .green: - return UIColor(rgb: 0x00e432) - case .teal: - return UIColor(rgb: 0x1fa9ab) - case .purple: - return UIColor(rgb: 0x7300aa) - case .pink: - return UIColor(rgb: 0xf9bec5) - case .brown: - return UIColor(rgb: 0x734021) - case .black: - return UIColor(rgb: 0x000000) - case .gray: - return UIColor(rgb: 0x5c585f) - case .white: - return UIColor(rgb: 0xffffff) - } - } - - func localizedString(strings: PresentationStrings) -> String { - switch self { - case .blue: - return strings.WallpaperSearch_ColorBlue - case .red: - return strings.WallpaperSearch_ColorRed - case .orange: - return strings.WallpaperSearch_ColorOrange - case .yellow: - return strings.WallpaperSearch_ColorYellow - case .green: - return strings.WallpaperSearch_ColorGreen - case .teal: - return strings.WallpaperSearch_ColorTeal - case .purple: - return strings.WallpaperSearch_ColorPurple - case .pink: - return strings.WallpaperSearch_ColorPink - case .brown: - return strings.WallpaperSearch_ColorBrown - case .black: - return strings.WallpaperSearch_ColorBlack - case .gray: - return strings.WallpaperSearch_ColorGray - case .white: - return strings.WallpaperSearch_ColorWhite - } - } -} - -enum WallpaperSearchQuery: Equatable { - case generic(String) - case color(WallpaperSearchColor, String) - - var botQuery: String { - switch self { - case let .generic(query): - return query - case let .color(color, query): - return "#color\(color.string) \(query)" - } - } - - var query: String { - switch self { - case let .generic(query), let .color(_, query): - return query - } - } - - func updatedWithText(_ text: String) -> WallpaperSearchQuery { - switch self { - case .generic: - return .generic(text) - case let .color(color, _): - return .color(color, text) - } - } - - func updatedWithColor(_ color: WallpaperSearchColor?) -> WallpaperSearchQuery { - if let color = color { - switch self { - case let .generic(text): - return .color(color, text) - case let .color(_, text): - return .color(color, text) - } - } else { - switch self { - case .generic: - return self - case let .color(_, text): - return .generic(text) - } - } - } -} - -final class ThemeGridSearchInteraction { - let openResult: (ChatContextResult) -> Void - let selectColor: (WallpaperSearchColor) -> Void - let setSearchQuery: (WallpaperSearchQuery) -> Void - let deleteRecentQuery: (String) -> Void - - init(openResult: @escaping (ChatContextResult) -> Void, selectColor: @escaping (WallpaperSearchColor) -> Void, setSearchQuery: @escaping (WallpaperSearchQuery) -> Void, deleteRecentQuery: @escaping (String) -> Void) { - self.openResult = openResult - self.selectColor = selectColor - self.setSearchQuery = setSearchQuery - self.deleteRecentQuery = deleteRecentQuery - } -} - -private enum ThemeGridRecentEntryStableId: Hashable { - case colors - case query(String) -} - -private enum ThemeGridRecentEntry: Comparable, Identifiable { - case colors(PresentationTheme, PresentationStrings) - case query(Int, String) - - var stableId: ThemeGridRecentEntryStableId { - switch self { - case .colors: - return .colors - case let .query(_, query): - return .query(query) - } - } - - static func ==(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { - switch lhs { - case let .colors(lhsTheme, lhsStrings): - if case let .colors(rhsTheme, rhsStrings) = rhs { - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - return true - } else { - return false - } - case let .query(lhsIndex, lhsQuery): - if case .query(lhsIndex, lhsQuery) = rhs { - return true - } else { - return false - } - } - } - - static func <(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { - switch lhs { - case .colors: - return true - case let .query(lhsIndex, _): - switch rhs { - case .colors: - return false - case let .query(rhsIndex, _): - return lhsIndex <= rhsIndex - } - } - } - - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ListViewItem { - switch self { - case let .colors(theme, strings): - return ThemeGridSearchColorsItem(account: account, theme: theme, strings: strings, colorSelected: { color in - interaction.selectColor(color) - }) - case let .query(_, query): - return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: query, tapped: { query in - interaction.setSearchQuery(.generic(query)) - }, deleted: { query in - interaction.deleteRecentQuery(query) - }, header: header) - } - } -} - -private struct ThemeGridSearchContainerRecentTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private struct ThemeGridSearchEntry: Comparable, Identifiable { - let index: Int - let result: ChatContextResult - - static func ==(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { - return lhs.index == rhs.index && lhs.result == rhs.result - } - - static func <(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { - return lhs.index < rhs.index - } - - var stableId: Int { - return self.index - } - - func item(account: Account, theme: PresentationTheme, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchItem { - return ThemeGridSearchItem(account: account, theme: theme, result: self.result, interaction: interaction) - } -} - -struct ThemeGridSearchContainerTransition { - let deletions: [Int] - let insertions: [GridNodeInsertItem] - let updates: [GridNodeUpdateItem] - let displayingResults: Bool - let isEmpty: Bool - let query: String -} - -private func themeGridSearchContainerPreparedRecentTransition(from fromEntries: [ThemeGridRecentEntry], to toEntries: [ThemeGridRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ThemeGridSearchContainerRecentTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } - - return ThemeGridSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -private func themeGridSearchContainerPreparedTransition(from fromEntries: [ThemeGridSearchEntry], to toEntries: [ThemeGridSearchEntry], displayingResults: Bool, account: Account, theme: PresentationTheme, isEmpty: Bool, query: String, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchContainerTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interaction: interaction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interaction: interaction)) } - - return ThemeGridSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, query: query) -} - -private struct ThemeGridSearchResult { - let query: String - let collection: ChatContextResultCollection - let items: [ChatContextResult] - let nextOffset: String? -} - -private struct ThemeGridSearchContext { - let result: ThemeGridSearchResult - let loadMoreIndex: String? -} - -final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { - private let context: AccountContext - - private let recentListNode: ListView - private let gridNode: GridNode - private let dimNode: ASDisplayNode - - private let emptyResultsTitleNode: ImmediateTextNode - private let emptyResultsTextNode: ImmediateTextNode - - private var enqueuedRecentTransitions: [(ThemeGridSearchContainerRecentTransition, Bool)] = [] - private var enqueuedTransitions: [(ThemeGridSearchContainerTransition, Bool)] = [] - private var validLayout: (ContainerViewLayout, CGFloat)? - - private var queryValue: WallpaperSearchQuery = .generic("") - private let queryPromise: Promise - private let searchDisposable = MetaDisposable() - private var recentDisposable: Disposable? - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private let presentationDataPromise: Promise - - private let _isSearching = ValuePromise(false, ignoreRepeated: true) - override var isSearching: Signal { - return self._isSearching.get() - } - - init(context: AccountContext, openResult: @escaping (ChatContextResult) -> Void) { - self.context = context - self.queryPromise = Promise(self.queryValue) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationData = presentationData - self.presentationDataPromise = Promise(self.presentationData) - - self.dimNode = ASDisplayNode() - self.recentListNode = ListViewImpl() - self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - self.recentListNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.gridNode = GridNode() - - self.emptyResultsTitleNode = ImmediateTextNode() - self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) - self.emptyResultsTitleNode.textAlignment = .center - self.emptyResultsTitleNode.isHidden = true - - self.emptyResultsTextNode = ImmediateTextNode() - self.emptyResultsTextNode.maximumNumberOfLines = 0 - self.emptyResultsTextNode.textAlignment = .center - self.emptyResultsTextNode.isHidden = true - - super.init() - - self.dimNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor - - self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - - self.addSubnode(self.dimNode) - self.addSubnode(self.recentListNode) - self.addSubnode(self.gridNode) - - self.addSubnode(self.emptyResultsTitleNode) - self.addSubnode(self.emptyResultsTextNode) - - let searchContext = Promise(nil) - let searchContextValue = Atomic(value: nil) - let updateSearchContext: ((ThemeGridSearchContext?) -> (ThemeGridSearchContext?, Bool)) -> Void = { f in - var shouldUpdate = false - let updated = searchContextValue.modify { current in - let (u, s) = f(current) - shouldUpdate = s - if s { - return u - } else { - return current - } - } - if shouldUpdate { - searchContext.set(.single(updated)) - } - } - - self.gridNode.isHidden = true - self.gridNode.visibleItemsUpdated = { visibleItems in - if let bottom = visibleItems.bottom { - if let context = searchContextValue.with({ $0 }), bottom.0 >= context.result.items.count - 8 { - updateSearchContext { previous in - guard let previous = previous else { - return (nil, false) - } - if previous.loadMoreIndex != nil { - return (previous, false) - } - guard let _ = previous.result.items.last else { - return (previous, false) - } - return (ThemeGridSearchContext(result: previous.result, loadMoreIndex: previous.result.nextOffset), true) - } - } - } - } - self.recentListNode.isHidden = false - - let previousSearchItems = Atomic<[ThemeGridSearchEntry]?>(value: nil) - - let interaction = ThemeGridSearchInteraction(openResult: { [weak self] result in - openResult(result) - if let strongSelf = self { - strongSelf.dismissInput?() - - let query = strongSelf.queryValue.query - if !query.isEmpty { - let _ = addRecentWallpaperSearchQuery(engine: strongSelf.context.engine, string: query).start() - } - } - }, selectColor: { [weak self] color in - self?.updateQuery({ $0.updatedWithColor(color) }, updateInterface: true) - }, setSearchQuery: { [weak self] query in - self?.dismissInput?() - self?.updateQuery({ _ in - return query - }, updateInterface: true) - }, deleteRecentQuery: { query in - let _ = removeRecentWallpaperSearchQuery(engine: context.engine, string: query).start() - }) - - let configuration = self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - - let foundItems = self.queryPromise.get() - |> mapToSignal { query -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - let query = query.botQuery - guard !query.isEmpty else { - return .single(nil) - } - - let wallpaperQuery = "#wallpaper \(query)" - updateSearchContext { _ in - return (nil, true) - } - - return .single(([], true)) - |> then( - configuration - |> mapToSignal { configuration -> Signal in - guard let name = configuration.imageBotUsername else { - return .single(nil) - } - return context.engine.peers.resolvePeerByName(name: name, referrer: nil) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> mapToSignal { peer -> Signal in - if let peer = peer { - return .single(peer._asPeer()) - } else { - return .single(nil) - } - } - } - |> mapToSignal { peer -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let loadMore = searchContext.get() - |> mapToSignal { searchContext -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - if let searchContext = searchContext { - if let _ = searchContext.loadMoreIndex, let nextOffset = searchContext.result.nextOffset { - let collection = searchContext.result.collection - let geoPoint = collection.geoPoint.flatMap { geoPoint -> (Double, Double) in - return (geoPoint.latitude, geoPoint.longitude) - } - return self.context.engine.messages.requestChatContextResults(botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset) - |> map { results -> ChatContextResultCollection? in - return results?.results - } - |> `catch` { error -> Signal in - return .single(nil) - } - |> map { nextResults -> (ChatContextResultCollection, String?) in - var results: [ChatContextResult] = [] - var existingIds = Set() - for result in searchContext.result.items { - results.append(result) - existingIds.insert(result.id) - } - var nextOffset: String? - if let nextResults = nextResults { - for result in nextResults.results { - if !existingIds.contains(result.id) { - results.append(result) - existingIds.insert(result.id) - } - } - if let newNextOffset = nextResults.nextOffset, !newNextOffset.isEmpty { - nextOffset = newNextOffset - } - } - let merged = ChatContextResultCollection(botId: collection.botId, peerId: collection.peerId, query: collection.query, geoPoint: collection.geoPoint, queryId: nextResults?.queryId ?? collection.queryId, nextOffset: nextOffset ?? "", presentation: collection.presentation, switchPeer: collection.switchPeer, webView: collection.webView, results: results, cacheTimeout: collection.cacheTimeout) - return (merged, nextOffset) - } - |> mapToSignal { newCollection, nextOffset -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - updateSearchContext { previous in - return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: searchContext.result.query, collection: newCollection, items: newCollection.results, nextOffset: nextOffset), loadMoreIndex: nil), true) - } - return .complete() - } - } else { - var entries: [ThemeGridSearchEntry] = [] - var i = 0 - for result in searchContext.result.items { - entries.append(ThemeGridSearchEntry(index: i, result: result)) - i += 1 - } - return .single((entries, false)) - } - } else { - return .complete() - } - } - - - return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue())) - |> then( - requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) - |> map { results -> ChatContextResultCollection? in - return results?.results - } - |> map { collection -> ([ThemeGridSearchEntry], Bool)? in - guard let collection = collection else { - return nil - } - var entries: [ThemeGridSearchEntry] = [] - var i = 0 - for result in collection.results { - entries.append(ThemeGridSearchEntry(index: i, result: result)) - i += 1 - } - updateSearchContext { _ in - return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: wallpaperQuery, collection: collection, items: collection.results, nextOffset: collection.nextOffset), loadMoreIndex: nil), true) - } - return (entries, false) - } - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - |> then(loadMore) - ) - } else { - return .single(nil) - } - } - ) - } - - let previousRecentItems = Atomic<[ThemeGridRecentEntry]?>(value: nil) - self.recentDisposable = (combineLatest(wallpaperSearchRecentQueries(engine: self.context.engine), self.presentationDataPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] queries, presentationData in - if let strongSelf = self { - var entries: [ThemeGridRecentEntry] = [] - - entries.append(.colors(presentationData.theme, presentationData.strings)) - for i in 0 ..< queries.count { - entries.append(.query(i, queries[i])) - } - - let header = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: { _ in - let _ = clearRecentWallpaperSearchQueries(engine: strongSelf.context.engine).start() - }) - - let previousEntries = previousRecentItems.swap(entries) - let transition = themeGridSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, header: header) - strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil) - } - }) - - self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get(), self.queryPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags, presentationData, query in - if let strongSelf = self { - strongSelf._isSearching.set(entriesAndFlags?.1 ?? false) - - let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) - - var isEmpty = false - if let entriesAndFlags = entriesAndFlags { - isEmpty = entriesAndFlags.0.isEmpty && !entriesAndFlags.1 - } - - let firstTime = previousEntries == nil - let transition = themeGridSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: context.account, theme: presentationData.theme, isEmpty: isEmpty, query: query.query, interaction: interaction) - strongSelf.enqueueTransition(transition, firstTime: firstTime) - } - })) - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - - strongSelf.presentationData = presentationData - strongSelf.presentationDataPromise.set(.single(presentationData)) - - if previousTheme !== presentationData.theme { - strongSelf.updateTheme(theme: presentationData.theme) - } - } - }) - - self.recentListNode.beganInteractiveDragging = { [weak self] _ in - self?.dismissInput?() - } - - self.gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - } - - override func didLoad() { - super.didLoad() - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancel?() - } - } - - deinit { - self.searchDisposable.dispose() - self.recentDisposable?.dispose() - self.presentationDataDisposable?.dispose() - } - - private func updateTheme(theme: PresentationTheme) { - self.backgroundColor = theme.chatList.backgroundColor - self.dimNode.backgroundColor = theme.chatList.backgroundColor - self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor - } - - private func updateQuery(_ f: (WallpaperSearchQuery) -> (WallpaperSearchQuery), updateInterface: Bool = false) { - let query = f(self.queryValue) - if query != self.queryValue { - self.queryValue = query - self.queryPromise.set(.single(query)) - - if updateInterface { - let tokens: [SearchBarToken] - let text: String - let placeholder: String - switch query { - case let .generic(query): - tokens = [] - text = query - placeholder = self.presentationData.strings.Wallpaper_Search - case let .color(color, query): - let backgroundColor = color.displayColor - let foregroundColor: UIColor - let strokeColor: UIColor - if color == .white { - foregroundColor = .black - strokeColor = self.presentationData.theme.rootController.navigationSearchBar.inputClearButtonColor - } else { - foregroundColor = .white - strokeColor = color.displayColor - } - tokens = [SearchBarToken(id: 0, icon: UIImage(bundleImageName: "Settings/WallpaperSearchColorIcon"), title: color.localizedString(strings: self.presentationData.strings), style: SearchBarToken.Style(backgroundColor: backgroundColor, foregroundColor: foregroundColor, strokeColor: strokeColor), permanent: false)] - text = query - placeholder = self.presentationData.strings.Wallpaper_SearchShort - } - self.setQuery?(nil, tokens, text) - self.setPlaceholder?(placeholder) - } - } - } - - override func searchTextUpdated(text: String) { - self.updateQuery({ $0.updatedWithText(text) }) - } - - override func searchTextClearPrefix() { - self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) - } - - override func searchTextClearTokens() { - self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) - } - - private func enqueueRecentTransition(_ transition: ThemeGridSearchContainerRecentTransition, firstTime: Bool) { - self.enqueuedRecentTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - } - } - - private func dequeueRecentTransition() { - if let (transition, firstTime) = self.enqueuedRecentTransitions.first { - self.enqueuedRecentTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.PreferSynchronousDrawing) - } else { - options.insert(.AnimateInsertion) - } - - self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - } - - private func enqueueTransition(_ transition: ThemeGridSearchContainerTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - let displayingResults = transition.displayingResults - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.gridNode.isHidden = !displayingResults - strongSelf.recentListNode.isHidden = displayingResults - strongSelf.dimNode.isHidden = displayingResults - strongSelf.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor - - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.WebSearch_SearchNoResultsDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - - let emptyResults = displayingResults && transition.isEmpty - strongSelf.emptyResultsTitleNode.isHidden = !emptyResults - strongSelf.emptyResultsTextNode.isHidden = !emptyResults - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - }) - } - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - let hadValidLayout = self.validLayout != nil - self.validLayout = (layout, navigationBarHeight) - - let minSpacing: CGFloat = 8.0 - let referenceImageSize: CGSize - let screenWidth = min(layout.size.width, layout.size.height) - if screenWidth >= 390.0 { - referenceImageSize = CGSize(width: 108.0, height: 230.0) - } else { - referenceImageSize = CGSize(width: 91.0, height: 161.0) - } - let imageCount = Int((layout.size.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) - let spacing = floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) - - let topInset = navigationBarHeight - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - - self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight + spacing, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - - let padding: CGFloat = 16.0 - let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - let insets = layout.insets(options: [.input]) - let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing - let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) - - transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) - transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) - - if !hadValidLayout { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func clearRecentSearch() { - let _ = (self.context.engine.peers.clearRecentlySearchedPeers() |> deliverOnMainQueue).start() - } - - override func scrollToTop() { - if !self.gridNode.isHidden { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - } else { - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift deleted file mode 100644 index 4913bcdb8b..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramCore -import SwiftSignalKit -import AsyncDisplayKit -import TelegramPresentationData -import PhotoResources - -final class ThemeGridSearchItem: GridItem { - let account: Account - let theme: PresentationTheme - let result: ChatContextResult - let interaction: ThemeGridSearchInteraction - - let section: GridSection? = nil - - init(account: Account, theme: PresentationTheme, result: ChatContextResult, interaction: ThemeGridSearchInteraction) { - self.account = account - self.theme = theme - self.result = result - self.interaction = interaction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = ThemeGridSearchItemNode() - node.setup(item: self) - return node - } - - func update(node: GridItemNode) { - guard let node = node as? ThemeGridSearchItemNode else { - assertionFailure() - return - } - node.setup(item: self) - } -} - -final class ThemeGridSearchItemNode: GridItemNode { - private let imageNode: TransformImageNode - - private(set) var item: ThemeGridSearchItem? - private var currentDimensions: CGSize? - - override init() { - self.imageNode = TransformImageNode() - self.imageNode.contentAnimations = [.subsequentUpdates] - self.imageNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.imageNode) - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - func setup(item: ThemeGridSearchItem) { - if self.item !== item { - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions: CGSize? - var immediateThumbnailData: Data? - switch item.result { - case let .externalReference(externalReference): - if let content = externalReference.content, externalReference.type != "gif" { - imageResource = content.resource - } else if let thumbnail = externalReference.thumbnail { - imageResource = thumbnail.resource - } - imageDimensions = externalReference.content?.dimensions?.cgSize - case let .internalReference(internalReference): - if let image = internalReference.image { - immediateThumbnailData = image.immediateThumbnailData - if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 321, height: 321)) { - imageResource = representation.resource - imageDimensions = representation.dimensions.cgSize - } - if let file = internalReference.file { - if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } else { - if let thumbnailRepresentation = smallestImageRepresentation(image.representations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } else if let file = internalReference.file { - immediateThumbnailData = file.immediateThumbnailData - if let dimensions = file.dimensions { - imageDimensions = dimensions.cgSize - } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - 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, 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, userLocation: .other, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512)) - } else { - updateImageSignal = .complete() - } - - if let updateImageSignal = updateImageSignal { - self.imageNode.setSignal(updateImageSignal) - } - - self.currentDimensions = imageDimensions - if let _ = imageDimensions { - self.setNeedsLayout() - } - } - - self.item = item - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let item = self.item { - item.interaction.openResult(item.result) - } - } - } - - override func layout() { - super.layout() - - let bounds = self.bounds - self.imageNode.frame = bounds - - if let item = self.item, let dimensions = self.currentDimensions { - let imageSize = dimensions.aspectFilled(bounds.size) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: bounds.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))() - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift deleted file mode 100644 index 3370f59479..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import TelegramCore -import SwiftSignalKit -import TelegramUIPreferences - -private struct WallpaperSearchRecentQueryItemId { - public let rawValue: EngineMemoryBuffer - - var value: String { - return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" - } - - init(_ rawValue: EngineMemoryBuffer) { - self.rawValue = rawValue - } - - init?(_ value: String) { - if let data = value.data(using: .utf8) { - self.rawValue = EngineMemoryBuffer(data: data) - } else { - return nil - } - } -} - -public final class RecentWallpaperSearchQueryItem: Codable { - public init() { - } - - public init(from decoder: Decoder) throws { - } - - public func encode(to encoder: Encoder) throws { - } -} - -func addRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WallpaperSearchRecentQueryItemId(string) { - return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue, item: RecentWallpaperSearchQueryItem(), removeTailIfCountExceeds: 100) - } else { - return .complete() - } -} - -func removeRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WallpaperSearchRecentQueryItemId(string) { - return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue) - } else { - return .complete() - } -} - -func clearRecentWallpaperSearchQueries(engine: TelegramEngine) -> Signal { - return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries) -} - -func wallpaperSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> { - return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries)) - |> map { items -> [String] in - var result: [String] = [] - for item in items { - let value = WallpaperSearchRecentQueryItemId(item.id).value - result.append(value) - } - return result - } -} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a9c17a2fa4..848ae4f389 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -2766,9 +2766,9 @@ final class ShareWithPeersScreenComponent: Component { transition: transition, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index d9cffb9fe4..204e0d7fa3 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -42,11 +42,21 @@ public final class SliderComponent: Component { public final class Continuous: Equatable { public let value: CGFloat public let minValue: CGFloat? + public let range: ClosedRange + public let startValue: CGFloat public let valueUpdated: (CGFloat) -> Void - public init(value: CGFloat, minValue: CGFloat? = nil, valueUpdated: @escaping (CGFloat) -> Void) { + public init( + value: CGFloat, + minValue: CGFloat? = nil, + range: ClosedRange = 0.0 ... 1.0, + startValue: CGFloat = 0.0, + valueUpdated: @escaping (CGFloat) -> Void + ) { self.value = value self.minValue = minValue + self.range = range + self.startValue = startValue self.valueUpdated = valueUpdated } @@ -57,6 +67,12 @@ public final class SliderComponent: Component { if lhs.minValue != rhs.minValue { return false } + if lhs.range != rhs.range { + return false + } + if lhs.startValue != rhs.startValue { + return false + } return true } } @@ -73,6 +89,10 @@ public final class SliderComponent: Component { public let minTrackForegroundColor: UIColor? public let knobSize: CGFloat? public let knobColor: UIColor? + public let isEnabled: Bool + public let trackHeight: CGFloat? + public let displaysBorderOnTracking: Bool + public let useLegacyKnob: Bool public let isTrackingUpdated: ((Bool) -> Void)? public init( @@ -83,6 +103,10 @@ public final class SliderComponent: Component { minTrackForegroundColor: UIColor? = nil, knobSize: CGFloat? = nil, knobColor: UIColor? = nil, + isEnabled: Bool = true, + trackHeight: CGFloat? = nil, + displaysBorderOnTracking: Bool = false, + useLegacyKnob: Bool = false, isTrackingUpdated: ((Bool) -> Void)? = nil ) { self.content = content @@ -92,6 +116,10 @@ public final class SliderComponent: Component { self.minTrackForegroundColor = minTrackForegroundColor self.knobSize = knobSize self.knobColor = knobColor + self.isEnabled = isEnabled + self.trackHeight = trackHeight + self.displaysBorderOnTracking = displaysBorderOnTracking + self.useLegacyKnob = useLegacyKnob self.isTrackingUpdated = isTrackingUpdated } @@ -114,6 +142,18 @@ public final class SliderComponent: Component { if lhs.knobColor != rhs.knobColor { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.trackHeight != rhs.trackHeight { + return false + } + if lhs.displaysBorderOnTracking != rhs.displaysBorderOnTracking { + return false + } + if lhs.useLegacyKnob != rhs.useLegacyKnob { + return false + } return true } @@ -123,6 +163,8 @@ public final class SliderComponent: Component { public final class View: UIView { private var nativeSliderView: SliderView? + private let nativeTrackBackgroundView = UIView() + private let nativeTrackForegroundView = UIView() private var sliderView: TGPhotoEditorSliderView? private var component: SliderComponent? @@ -139,6 +181,12 @@ public final class SliderComponent: Component { required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private static let legacyKnobImage: UIImage = generateImage(CGSize(width: 21.0, height: 21.0), rotatedContext: { _, context in + context.setShadow(offset: CGSize(width: 0.0, height: 0.5), blur: 1.5, color: UIColor(white: 0.0, alpha: 0.5).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: 17.0, height: 17.0)) + })! public func cancelGestures() { if let sliderView = self.sliderView, let gestureRecognizers = sliderView.gestureRecognizers { @@ -158,6 +206,11 @@ public final class SliderComponent: Component { let size = CGSize(width: availableSize.width, height: 44.0) if #available(iOS 26.0, *), component.useNative { + if let sliderView = self.sliderView { + self.sliderView = nil + sliderView.removeFromSuperview() + } + let sliderView: SliderView if let current = self.nativeSliderView { sliderView = current @@ -167,13 +220,15 @@ public final class SliderComponent: Component { sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) sliderView.layer.allowsGroupOpacity = true + self.addSubview(self.nativeTrackBackgroundView) + self.addSubview(self.nativeTrackForegroundView) self.addSubview(sliderView) self.nativeSliderView = sliderView switch component.content { case let .continuous(continuous): - sliderView.minimumValue = Float(continuous.minValue ?? 0.0) - sliderView.maximumValue = 1.0 + sliderView.minimumValue = Float(continuous.minValue ?? continuous.range.lowerBound) + sliderView.maximumValue = Float(continuous.range.upperBound) case let .discrete(discrete): sliderView.minimumValue = 0.0 sliderView.maximumValue = Float(discrete.valueCount - 1) @@ -182,20 +237,69 @@ public final class SliderComponent: Component { } switch component.content { case let .continuous(continuous): + sliderView.minimumValue = Float(continuous.minValue ?? continuous.range.lowerBound) + sliderView.maximumValue = Float(continuous.range.upperBound) sliderView.value = Float(continuous.value) case let .discrete(discrete): + sliderView.minimumValue = 0.0 + sliderView.maximumValue = Float(discrete.valueCount - 1) sliderView.value = Float(discrete.value) } - sliderView.minimumTrackTintColor = component.trackForegroundColor - sliderView.maximumTrackTintColor = component.trackBackgroundColor + + let useCenteredNativeTrack: Bool + if case let .continuous(continuous) = component.content, continuous.range.lowerBound < continuous.startValue && continuous.startValue < continuous.range.upperBound { + useCenteredNativeTrack = true + } else { + useCenteredNativeTrack = false + } + + if useCenteredNativeTrack { + sliderView.minimumTrackTintColor = UIColor.clear + sliderView.maximumTrackTintColor = UIColor.clear + + let trackHeight = component.trackHeight ?? 4.0 + let trackFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - trackHeight) * 0.5)), size: CGSize(width: availableSize.width, height: trackHeight)) + self.nativeTrackBackgroundView.backgroundColor = component.trackBackgroundColor + self.nativeTrackBackgroundView.layer.cornerRadius = trackHeight * 0.5 + self.nativeTrackBackgroundView.alpha = component.isEnabled ? 1.0 : 0.3 + transition.setFrame(view: self.nativeTrackBackgroundView, frame: trackFrame) + + if case let .continuous(continuous) = component.content { + let rangeDistance = max(continuous.range.upperBound - continuous.range.lowerBound, CGFloat.ulpOfOne) + let startPosition = min(max((continuous.startValue - continuous.range.lowerBound) / rangeDistance, 0.0), 1.0) * trackFrame.width + let valuePosition = min(max((continuous.value - continuous.range.lowerBound) / rangeDistance, 0.0), 1.0) * trackFrame.width + let foregroundFrame = CGRect( + origin: CGPoint(x: trackFrame.minX + min(startPosition, valuePosition), y: trackFrame.minY), + size: CGSize(width: abs(valuePosition - startPosition), height: trackFrame.height) + ) + self.nativeTrackForegroundView.backgroundColor = component.trackForegroundColor + self.nativeTrackForegroundView.layer.cornerRadius = trackHeight * 0.5 + self.nativeTrackForegroundView.alpha = component.isEnabled ? 1.0 : 0.3 + transition.setFrame(view: self.nativeTrackForegroundView, frame: foregroundFrame) + } + } else { + sliderView.minimumTrackTintColor = component.trackForegroundColor + sliderView.maximumTrackTintColor = component.trackBackgroundColor + self.nativeTrackBackgroundView.frame = CGRect() + self.nativeTrackForegroundView.frame = CGRect() + } + sliderView.isEnabled = component.isEnabled + sliderView.alpha = component.isEnabled ? 1.0 : 0.3 transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0))) } else { + self.nativeTrackBackgroundView.frame = CGRect() + self.nativeTrackForegroundView.frame = CGRect() + if let nativeSliderView = self.nativeSliderView { + self.nativeSliderView = nil + nativeSliderView.removeFromSuperview() + } + var internalIsTrackingUpdated: ((Bool) -> Void)? if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in if let self { - if !"".isEmpty { + if component.displaysBorderOnTracking { if isTracking { self.sliderView?.bordered = true } else { @@ -215,51 +319,20 @@ public final class SliderComponent: Component { } else { sliderView = TGPhotoEditorSliderView() sliderView.enablePanHandling = true - if let knobSize = component.knobSize { - sliderView.lineSize = knobSize + 4.0 - } else { - sliderView.lineSize = 4.0 - } - sliderView.trackCornerRadius = sliderView.lineSize * 0.5 sliderView.dotSize = 5.0 - sliderView.minimumValue = 0.0 - sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true switch component.content { case let .discrete(discrete): - sliderView.maximumValue = CGFloat(discrete.valueCount - 1) sliderView.positionsCount = discrete.valueCount sliderView.useLinesForPositions = true sliderView.markPositions = discrete.markPositions case .continuous: - sliderView.maximumValue = 1.0 + break } sliderView.backgroundColor = nil sliderView.isOpaque = false - sliderView.backColor = component.trackBackgroundColor - sliderView.startColor = component.trackBackgroundColor - sliderView.trackColor = component.trackForegroundColor - if let knobSize = component.knobSize { - sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) - if let knobColor = component.knobColor { - context.setFillColor(knobColor.cgColor) - } else { - context.setFillColor(UIColor.white.cgColor) - } - context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize))) - }) - } else { - sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) - }) - } sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) @@ -271,22 +344,66 @@ public final class SliderComponent: Component { self.sliderView = sliderView self.addSubview(sliderView) } + if let trackHeight = component.trackHeight { + sliderView.lineSize = trackHeight + } else if let knobSize = component.knobSize { + sliderView.lineSize = knobSize + 4.0 + } else { + sliderView.lineSize = 4.0 + } + sliderView.trackCornerRadius = sliderView.lineSize * 0.5 + sliderView.backColor = component.trackBackgroundColor + sliderView.startColor = component.useLegacyKnob ? UIColor(rgb: 0xffffff) : component.trackBackgroundColor + sliderView.trackColor = component.trackForegroundColor + sliderView.isUserInteractionEnabled = component.isEnabled + sliderView.alpha = component.isEnabled ? (component.useLegacyKnob ? 1.3 : 1.0) : 0.3 + if let knobSize = component.knobSize { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + if let knobColor = component.knobColor { + context.setFillColor(knobColor.cgColor) + } else { + context.setFillColor(UIColor.white.cgColor) + } + context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize))) + }) + } else if component.useLegacyKnob { + sliderView.knobImage = View.legacyKnobImage + } else { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) + } sliderView.lowerBoundTrackColor = component.minTrackForegroundColor switch component.content { case let .discrete(discrete): - sliderView.value = CGFloat(discrete.value) + sliderView.minimumValue = 0.0 + sliderView.maximumValue = CGFloat(discrete.valueCount - 1) + sliderView.startValue = 0.0 + sliderView.positionsCount = discrete.valueCount + sliderView.useLinesForPositions = true + sliderView.markPositions = discrete.markPositions if let minValue = discrete.minValue { sliderView.lowerBoundValue = CGFloat(minValue) } else { sliderView.lowerBoundValue = 0.0 } + sliderView.value = CGFloat(discrete.value) case let .continuous(continuous): - sliderView.value = continuous.value + sliderView.minimumValue = continuous.range.lowerBound + sliderView.maximumValue = continuous.range.upperBound + sliderView.startValue = continuous.startValue + sliderView.positionsCount = 0 if let minValue = continuous.minValue { sliderView.lowerBoundValue = minValue } else { sliderView.lowerBoundValue = 0.0 } + sliderView.value = continuous.value } sliderView.interactionBegan = { internalIsTrackingUpdated?(true) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 3dfa33e15c..b73ff9df5a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -36,7 +36,6 @@ swift_library( "//submodules/PhoneNumberFormat", "//submodules/TelegramIntents", "//submodules/LegacyUI", - "//submodules/WebSearchUI", "//submodules/PremiumUI", "//submodules/ICloudResources", "//submodules/LegacyComponents", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index dcf7f6afa1..7b342f3c00 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -17,7 +17,6 @@ import TextFormat import PhoneNumberFormat import TelegramIntents import LegacyUI -import WebSearchUI import ChatTimerScreen import PremiumUI import ICloudResources @@ -1848,6 +1847,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let attachmentController = AttachmentController( context: component.context, updatedPresentationData: updatedPresentationData, + style: .glass, chatLocation: .peer(id: peer.id), buttons: buttons, initialButton: initialButton @@ -1981,7 +1981,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != component.context.account.peerId let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = LocationPickerController(context: component.context, updatedPresentationData: updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self, weak view] location, _, _, _, _ in + let controller = LocationPickerController(context: component.context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self, weak view] location, _, _, _, _ in guard let self, let view else { return } @@ -2160,58 +2160,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { })) return true case .gift: - /*let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions - if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in - if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) - } - }) - completion(controller, controller.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) - - let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() - }*/ - //TODO:gift controller break case let .app(bot): let _ = bot -// let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) -// let theme = component.theme -// let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) -// let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) -// controller.openUrl = { [weak self] url, _, _, _ in -// guard let self else { -// return -// } -// let _ = self -// //self?.openUrl(url, concealed: true, forceExternal: true) -// } -// controller.getNavigationController = { [weak view] in -// guard let view, let controller = view.component?.controller() else { -// return nil -// } -// return controller.navigationController as? NavigationController -// } -// controller.completion = { [weak self] in -// guard let self else { -// return -// } -// let _ = self -// /*if let strongSelf = self { -// strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { -// $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } -// }) -// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() -// }*/ -// } -// completion(controller, controller.mediaPickerContext) -// self.controllerNavigationDisposable.set(nil) default: break } @@ -2255,7 +2206,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let theme = component.theme - let controller = MediaPickerScreenImpl(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + let controller = MediaPickerScreenImpl(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: .glass, peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) let mediaPickerContext = controller.mediaPickerContext controller.openCamera = { [weak self, weak view] cameraView in guard let self, let view else { @@ -2265,29 +2216,6 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.openCamera(view: view, peer: peer, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, cameraView: cameraView) } } - controller.presentWebSearch = { [weak self, weak view, weak controller] mediaGroups, activateOnDisplay in - guard let self, let view, let controller else { - return - } - self.presentWebSearch(view: view, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in - controller?.present(c, in: .current) - if let webSearchController = c as? WebSearchController { - webSearchController.searchingUpdated = { [weak mediaGroups] searching in - if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded { - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0) - mediaGroups.displayNode.isUserInteractionEnabled = !searching - } - } - webSearchController.present(mediaGroups, in: .current) - webSearchController.dismissed = { - updateMediaPickerContext(mediaPickerContext) - } - controller?.webSearchController = webSearchController - updateMediaPickerContext(webSearchController.mediaPickerContext) - } - }) - } controller.presentSchedulePicker = { [weak self, weak view] media, done in guard let self, let view else { return @@ -2368,40 +2296,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak view, weak legacyController] in - if let view, let component = view.component { - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting, scheduleTime in - if let legacyController = legacyController { - legacyController.dismiss() - } - guard let view else { - return - } - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in - if let strongSelf = self, let view { - strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak view] signals in - if let strongSelf = self, let view { - if editingMedia { - strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals) - } else { - strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - })) - controller.getCaptionPanelView = { [weak view] in - guard let self, let view else { - return nil - } - return self.getCaptionPanelView(view: view, peer: peer) - } - component.controller()?.push(controller) - } - }, presentSelectionLimitExceeded: { [weak view] in + configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: nil, presentSelectionLimitExceeded: { [weak view] in guard let view, let component = view.component else { return } @@ -2671,44 +2566,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendMessage(scheduleTime) } - - private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { - guard let component = view.component else { - return - } - let context = component.context - let peer = component.slice.effectivePeer - let storyId = component.slice.item.storyItem.id - - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in - if let self { - let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in - if let self, let view { - self.performSendContextResultAction(view: view, results: results, result: result, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak self, weak view] signals in - if let self, let view, !signals.isEmpty { - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: EngineStoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }) - }), activateOnDisplay: activateOnDisplay) - controller.getCaptionPanelView = { [weak self, weak view] in - if let view { - return self?.getCaptionPanelView(view: view, peer: peer) - } else { - return nil - } - } - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } - + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreenImpl? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil @@ -2848,6 +2706,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return nil } return self.getCaptionPanelView(view: view, peer: peer) + }, photoToolbarView: { [context = component.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, dismissedWithResult: { [weak self] in guard let self else { return @@ -3429,12 +3289,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case let .url(url, concealed): if canOpenIn { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: component.context, item: .url(url: url), openUrl: { url in + let actionSheet = OpenInOptionsScreen(context: component.context, item: .url(url: url), openUrl: { url in if let navigationController = component.controller()?.navigationController as? NavigationController { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } }) - component.controller()?.present(actionSheet, in: .window(.root)) + component.controller()?.push(actionSheet) } else { openUrl(url, concealed) } diff --git a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD index 72a004e510..233f4b8e1a 100644 --- a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD +++ b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD @@ -17,7 +17,6 @@ swift_library( "//submodules/InstantPageUI", "//submodules/AccountContext", "//submodules/LocalMediaResources", - "//submodules/WebSearchUI", "//submodules/InstantPageCache", "//submodules/SettingsUI", "//submodules/WallpaperResources", diff --git a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift index 203fcd6656..9e0b0cb81f 100644 --- a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift +++ b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift @@ -5,7 +5,6 @@ import TelegramNotices import InstantPageUI import AccountContext import LocalMediaResources -import WebSearchUI import InstantPageCache import SettingsUI import WallpaperResources diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift index 03e2207b99..4b8f594141 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -304,6 +304,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.scrollNode = ASScrollNode() self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.scrollsToTop = false self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -312,6 +313,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) self.textFieldScrollNode = ASScrollNode() + self.textFieldScrollNode.view.scrollsToTop = false self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) diff --git a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift index 331f8b6f05..3dc429d3ac 100644 --- a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift +++ b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift @@ -260,6 +260,7 @@ public final class TranslationLanguagesContextMenuContent: ContextControllerItem self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json new file mode 100644 index 0000000000..d4f692b39a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c701eec2621f9e48f926d113448097efe9a850ea GIT binary patch literal 9535 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json new file mode 100644 index 0000000000..b96e295d1a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc9.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf new file mode 100644 index 0000000000000000000000000000000000000000..12da3dbe05d1af180bfbd4411d1d239fca873e3c GIT binary patch literal 11223 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json new file mode 100644 index 0000000000..4b76f1f60f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc7.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e511bf1e9abca92a5a884d2c7b022d88db66c266 GIT binary patch literal 10449 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json index 2468a09a36..0ddc25e04e 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "headerlocation_30.pdf", + "filename" : "loc1.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..df05f9d0997e8f3011393df04847b6d93243cffb GIT binary patch literal 7747 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json new file mode 100644 index 0000000000..2836db899e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/headermap_30.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/loc2.pdf similarity index 59% rename from submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/headermap_30.pdf rename to submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/loc2.pdf index 070e541975ae0ec621a4b3cb5dd8fffe1151f86a..788817e75327fef95327f1a3b59999902a76ee04 100644 GIT binary patch delta 3170 zcmbVOeN+_J6)ze?SS64~ff%)q6%yctosXT}*;$tr6o|NBaF;}h#xlD*up4G)w==V0 zA<=1511X@!`;qvphfB*!LxoO+ZT3QD!dri#YqSTwa_&56~tNk873S@r{( z{?Ykk&U<&>y}$dr@AuxF8^jUowuMS9C$z{uKmr^%bpSn>E9{orLma@?*44&L4eGp( zlUXk-v(RX_>m*s^*nrNqG!kMxt(*+pT#H{Q)9ZoXTc%%Qs>AC-%Q>HaMTf|_I#xA! zI$Ax9SHIM*vqh|tKqvsC0VNXP1<4vI)3e^7o3p|(vSWHX1hI+UW~;NZ8fc5)tW58d z<&YJ_+S=L-ZAL>-Y{3YIVKAJ;NU{{JD3#g;nT?bR(qc76EJ`IOc|?Cm_6G%kqO$H_ zScbVO8o?NX*AoW_g+*Qs@AY6D&jmO^mN3FVVEPmf$Rq9|NCuUJ!4nK%5jF&u;~EiE zR-4yjmHjf$A!gbG`6Xhyw3V zxM`N8NH%OL9KuKO!fZ=Y3W{3eWo%0#Ry+lJ5hr{jR+5W~l^R;hC#mRKcoi(k3L=WS z{UL92m_@2d+FjP~52zcUe#8U`^Lvx38FR6QSW&hUHY{bXR`|c>pc$d2pb9Z4!zG#y z;Vdp=PJoHDA)UfeOyqDBbU&`sp<{vu0MI10eG^_vS!kLuLMv_{a5H11Kq+n@DS{*j zKp04dwwOqDK$vjq?J-x{EWEO+l8hQ>eE)XMci>kuexIrIKR^8VdSXuJt;@v+_}q&p zkG)e+^n*)IUw+TFt7i>I%6<76f#|JQ51%NAK7aMfai87M_qD@5cDHc7x1J;)3PB7=3@;f!&t$T<^7@YXjTZ0zmMt|@tT-{1{X{m%1w{I=}$1J^HnZ*F)v??jbzZdHAj^RDwBt!{eq z^hdwT-0*bK?Cd?p%eydRO*z&1rTv2I<#WeZZyJ{x=**+fe474a^HBF@r8^^k74SZK zi9aLlJ$)`m-oE{{89Qp%jNdm99eK^SB5l0CySQ*xNt)~7BY6+}v~S?sImeb|Hm%4w z*nQWMS9V$U5)XoKUi3NLV7O+%!jY}Kw{g>NT!Y`5@9H1y=Dpe3U3U-L8(jSds~)^PNAtckR&rdETZyo(1nX-}{Nrnzvxyj+w@u z&D~9X_CU_=Ka;I;dHT;!uAKR4@9^UL+l#Y0Jw?r)cMDrf*4$TbD49PoyLV#O<=1ui zY@Fr?Y$YpNt&M{=BI4yRKgngBzP)+L~5A5h<+R zlV_Ndb)w?Q4S%>3`||j}p<}Iw8?Wu?8NaF=GTqzz#Rpf`4IKPByzzqlDc{%^(a~48 zoISaEiTzF2b8oG5>}L<|c?#cf^Lp>i3yT+xex^%bq%0fF`ggGJt%>vW&cBY`dg5;! z^I^vu|8oC*_Mc8=IHHkxiwewU8l4FB91y1XL9p2X=J0zZunrLBD4%0mW={E5AqYVk zetk^4_ONlc)^qCNbrth$VBHezPO+Iw7Q-G{ zizWd)x*+Es1$L!)hIC364wRk+nw%h>2q06s9>6Mto-n%TC-Ky#r{Pq=768r304SPv z7oZZ<1A_43sAF4*}>l_oa%!v#=9Cb1v!b8Vbh zFLKQsWQPt7@~`wuq3!u|n>5FG*m^PO4SP5d6jl3M0&Hya)J5opHK0Edp#4^8pxU>1 z3aNYm89kM=IL#EjnNvE0q@snnxMDHFLZAl-nL2fV;}#Rt>l7PfhH{^3vrO|PjPx{L zg2G`1Qhf=^2#+t0E6KnlZl1P~qA7STQk^N9R-Kc5DGTh`Q`cD-YO2j-rjnC}`0z2A zX?Psblc<7;rlv7AG1Dt!hTcSZ9m%oDp0)R`F^ L*A*64JyiW4-IKA1 delta 2042 zcmZWqeLPh88ZR$l)P%c@u-aiLd7E?Q)p)C+8774&Vfbjq9GNgPW+tSVa<8`9B{~VI z9n@4qNmwsst4m2z+bG#0y420w>n$%SvuV$C_kPx{=db5{&-490&*%63e4eMpeHzTO zh6$FdLm>fA$lZfN5wHMOn_#h?o>+XaSjgKO3yHA~O#bd9&Uyq!0>7OvO@y!x@eqdx ziIX^@D3C}c0@}ac-l+d85%W#Hfp-c6_O$-tpHqbw~?0@9i9Q|i2WQtQi38MU#Gc_5yhhr1xs+*JnV!c7$Vk8jXSv#R9tjh^pjCTAwc8M)Us z^vK&9x|`hOIjtYL;PI${>{?-=q<8katN56-%kJFH%5Mm}dC>!|pQA>C**|pjTgIN+ zxKF7blquGgd|O!@&c-IJ$c|?f_&nV++8GaN9AawMimUzy6fGCE});9bb2fzNT`v!CLaH=<6KZTa-}pPPOXfY8qkf zr8|8JmD-21W0!izo1)ld^fdi+#%8i~=2~^Bugq!N2b+7R*Bmw)sxJOfq~YPs|fiT}2#B8=P`QpLaUX6-0yP2a;^1x*l+lCC%AM77DE4iMIz8K$!@EI(RlhMiIB ztqF_BHIFa!HCno*uP&>kG!A!Qe3Lm{zi4MQDA+Fmy3$rPz0R|P&h^8$*$zjmZ!cn9XEE}Hl4AtgA*{&{gc$Bfw`wE z`XJkj*lRrb=xvw>rwYue*lF|vRDW3a}1&8e)Mn-+uTE}q>o7-8?|?^ z%1n0l)7y5Tnuy9Yw64Cem*%5_bRt`ZtS=sEW(|I*?lsMuz!YBAz31L^zq)3m2p8># z_r``?s;)ZTZ{y-}nZ2DYCNS*N9nED}{cGob(FA7m^j2u-bJ2=JU$0z7e>2k7F~BI6 zSNRH{7x~Rjl8TvQhwGery)J=C^16LD>eM(7ulVl3TETv+!>W0!V{Z4Chm>?@Gg#OE z#(CVl!`AR@O+kE7{+T6cyIxIaF_&b>jG4HapBg#4&9UGAW5zT(La7K6veRW~MFS>V zNx5`;YipCi-Lfs17yMcA#M)EAV5gpa=)hDr`p_=JZ@s)uD7!uOMaEu~&z*Qb z@qKvhqZ5I-23|vrtq-<1s@I%v_hOs11mGgv{D-`=hVBN`?nYZ#-Hqx@&ojoKf8N!X z6*87^upr^RO4?>;Yp1*0$SGK+$WxVuby%LYoenQ%u(&p?VYjXO84n(dbj$sM!c3;z z*UMg1LyqZrDFgyXE_x65sHC?qHIVIC4GU zK&jHl=za3Z;p=-a%$LHr*VAaz&|2gDw?d&4n@b3-?!AFKPc!yTB~oO=UJV)4`k*Jq z@%Zuh+Cwo7PXgfjhQIrtdk(r@DX>O8OD$k^RT*3`Qm1u=Y66e`T!=3m_}sJdj$A&w z@%c9?6C33B20sxqOY-O2lRqmz{F<_FZq?AO(X{ykAE)M8W1MBp^BEcQqmvJaZJ!LD zJhEVGm#IW5hB!$`1{+~a^*{od23uoR8`1>=p;Urp!=o4rxEVu(6&Q1tuTUU`1PBZg zk$oHT`??P1h!GGAQhvu4Ea@-UANxLl&qE}!AQ|??kYRswG%kz}r9$FhF%$^6R7VBew3uvRV~|K(R7e5{$UXXV6B0oBLSD!v0d9-AAQib}f98_O;KBis z{z#C?MB1W4s`kOP@yE-Msk8;JAQf}?iI5njy}qnXh!+kZK%qtfR00SRXr%RvzN57! s<3of(#9(Att?9N15Xw)7u%H_X@meC~h^1+WgD5f$q@e8W89~h705y9m6951J diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json index b67dccfbe4..8980d96ec7 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "headermap_30.pdf", + "filename" : "loc5.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4d604fb8c0c18a425b3504c1af2bf403542b7505 GIT binary patch literal 8152 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json new file mode 100644 index 0000000000..a347dfb45c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e06bf01f7a884ceb1dd35465e07482450321dcc7 GIT binary patch literal 7724 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 E0damL!2kdN literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json similarity index 83% rename from submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json index cb8e30c043..b3a12bd66e 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tools_30.pdf", + "filename" : "tools_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf new file mode 100644 index 0000000000000000000000000000000000000000..6a93dd33d38004402bad4d87480c8e642441e661 GIT binary patch literal 4094 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 Vun1r5p_k;OVTvFjA#G!w{{VmdeLMgF literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json index 6074e4a7f0..912db07593 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "pencil_30.pdf", + "filename" : "pencil_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf new file mode 100644 index 0000000000000000000000000000000000000000..211becd4a795898b334cfdc3c5ac310c2ac741e7 GIT binary patch literal 4181 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 -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.170021 4.169979 cm -1.000000 1.000000 1.000000 scn -1.660000 10.830000 m -1.660000 15.894451 5.765549 20.000000 10.830000 20.000000 c -15.894451 20.000000 20.000000 15.894451 20.000000 10.830000 c -20.000000 7.821688 18.551388 5.151716 16.313671 3.479595 c -14.640235 11.010053 l -14.555845 11.389809 14.219020 11.660000 13.830000 11.660000 c -13.553333 11.660000 l -11.778684 16.983950 l -11.474785 17.895645 10.185216 17.895649 9.881317 16.983952 c -8.106667 11.660000 l -7.830000 11.660000 l -7.440980 11.660000 7.104155 11.389809 7.019765 11.010053 c -5.346330 3.479595 l -3.108612 5.151716 1.660000 7.821688 1.660000 10.830000 c -h -13.164198 10.000000 m -14.815517 2.569059 l -13.610337 1.986547 12.258304 1.660000 10.830000 1.660000 c -9.401696 1.660000 8.049662 1.986547 6.844482 2.569059 c -8.495803 10.000000 l -13.164198 10.000000 l -h -10.830000 21.660000 m -4.848756 21.660000 0.000000 16.811243 0.000000 10.830000 c -0.000000 4.848755 4.848756 0.000000 10.830000 0.000000 c -16.811245 0.000000 21.660000 4.848755 21.660000 10.830000 c -21.660000 16.811243 16.811245 21.660000 10.830000 21.660000 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1162 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000001252 00000 n -0000001275 00000 n -0000001448 00000 n -0000001522 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1581 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json deleted file mode 100644 index 71cf2c0e13..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "undo.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png deleted file mode 100644 index 1dc7913d2b5419728676b6b392865c794bbeaae2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2175 zcmbVO2~gBl7+&NQq~hpQq0+{nvUc4~_5wGu(uGA1T|`(97^{XPKwvisNmh2D^;m6{ zwsO{bp+#$LMXflZ(^{)2IEt-cL94ZjS4W*Hg2l7#)V}4|QKxriUh-b@{ojB8lj4-* z_(1;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 diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf deleted file mode 100644 index 25301318c7..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf +++ /dev/null @@ -1,101 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.170021 4.169979 cm -1.000000 1.000000 1.000000 scn -5.830000 21.660000 m -6.288396 21.660000 6.660000 21.288397 6.660000 20.830000 c -6.660000 17.660000 l -20.830000 17.660000 l -21.288397 17.660000 21.660000 17.288397 21.660000 16.830000 c -21.660000 16.371603 21.288397 16.000000 20.830000 16.000000 c -6.660000 16.000000 l -6.660000 12.830000 l -6.660000 12.371604 6.288396 12.000000 5.830000 12.000000 c -5.371603 12.000000 5.000000 12.371604 5.000000 12.830000 c -5.000000 16.000000 l -0.830000 16.000000 l -0.371603 16.000000 0.000000 16.371603 0.000000 16.830000 c -0.000000 17.288397 0.371603 17.660000 0.830000 17.660000 c -5.000000 17.660000 l -5.000000 20.830000 l -5.000000 21.288397 5.371603 21.660000 5.830000 21.660000 c -h -20.830000 3.999998 m -21.288397 3.999998 21.660000 4.371601 21.660000 4.829998 c -21.660000 5.288395 21.288397 5.659998 20.830000 5.659998 c -16.660000 5.659998 l -16.660000 8.830000 l -16.660000 9.288396 16.288397 9.660000 15.830000 9.660000 c -15.371604 9.660000 15.000000 9.288396 15.000000 8.830000 c -15.000000 5.659998 l -0.830000 5.659998 l -0.371603 5.659998 0.000000 5.288395 0.000000 4.829998 c -0.000000 4.371601 0.371603 3.999998 0.830000 3.999998 c -15.000000 3.999998 l -15.000000 0.829998 l -15.000000 0.371603 15.371604 0.000000 15.830000 0.000000 c -16.288397 0.000000 16.660000 0.371603 16.660000 0.829998 c -16.660000 3.999998 l -20.830000 3.999998 l -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1452 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000001542 00000 n -0000001565 00000 n -0000001738 00000 n -0000001812 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1871 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json index 71cf2c0e13..7a253e3f25 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "undo.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "undo_30 (4).pdf", + "idiom" : "universal" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png deleted file mode 100644 index c2b1063a3960405be9f2db73426f549d12d6d3ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Q-Dv1t9yaZf6Bnu+zI_a(?m;x z{DK(@<{P|M5J-r>->~1I|NMdp8}ip*=vwc41*qVor;B4qMcmtKfmufkco+gLCqC8i zot%4l=l@XdrTbmkxOZ)s$859YiSxAZEVJ8Jwu`V|P+|8k%J~1rC*8`i>fUd~LO=g~ zS2%*Kx>va6->X{U`|s4XGE0U%{2u#F*&m#3Q`P*Xd~!#KZKm}~b%&F)JawHfCG5;w z$q>=@xIv`7d$<4IQfq?yZUbZZh67NZQEQG?_>vC xU7qxzjYV$S$F?hXA4Q8v)=hVgTXpk|^cE}qMQXd3NdZ02;OXk;vd$@?2>@GP&Jh3r diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/headerlocation_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo_30 (4).pdf similarity index 66% rename from submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/headerlocation_30.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo_30 (4).pdf index 3ed9097bd4a926272df7eadcc0d4731afae57c4a..deba9dc00c7c098e1b76e1c48639c3e5adef51f5 100644 GIT binary patch delta 2061 zcmZuydpwls9yc?t)3}dYDK9c#Ad2!p|J zR0T9Dm4Rf9nh{hf;=(_}0{{YLrIG6(3>KpSdwS43Q3mz^o%Ze+hUs`gEMLXNBk#~) zWwPIEhP*qbqutmI7^rRntBt|H=nEWzLF(+)O)+Q%7GKBFNWfzlXq&#NJ@Zd_C|nN@ zcV4`J4>&5(K;V5k2t=3lEiiP{+yF3FU0E3!P|f{ELlA@p7*H@&G{t}@&uyvNxSeo( zR+g?~UDP-!9;iBFqRu~2fa&FINqwBt)7sWCURXHQY#>I1xBh#%{7L-K(vKxWtmVe! z(UGOW`32JDX?fGK^P;Hm;mF*Pnnury)q_j-rL6gp_NKReZ`BTiq9MUzwULA?hbpH@31v+Xk9wJ=`h z+n}E9207dnTG8T1g4V@u)d_L9zFQ%@X+b}wsR-i-9t&%jb#=3u%J}Zw8^u~me=@Ft z%#42Nd8>lM=Y>p&h`Cz@4_aP;83DbW($b!%Hg~=hV}m;n^GG&na>ZUB+TI1*H?DQh zX)YH$J}s)DE$~Y}#a2JPXIfHV^)+ST)&aMWDDxnNctGSgP{tuO-%9*)@wQJ_jh{rM z6Bh26A2V+JZcex2rk$Lubh)|XDi{f}z;rW>-VYWjXZrX^^YF^xXc6v1#D^S&y%JHZcsmM1rEOPb?eLYJ+UM7D*c7 z*Ofi-J$0RPDbJ4TxqLUqUq?#?H{50x8~tQmW9+ntQw4OYF+05OIhRSx_cOUUi-cFZ z+vs+YIZ5X?^xoOIQ9Ut{ThM7^_gbyi|A?Pqil#AU>$!5>J6dvF(s4bl5s2=5+0*~T zwFGoPH=-@o<76a%GP^X+FXq~Sr?=!;J*V50^^85kjW}GSC|fc&H?;ROk==!S4e&Cy z+VIO%?c)u#>6ONw{ys9_A!$mgrLB`+xu$yn;mZ6Rt41`dk(I={(WLp0A2wtOWc{K3 z4*rt6kBgZXr1BC3_9m{mcyV8Bz-+FaM6@f~= z#FmeRxtp$1aQP>0u(iWW&ft9wx%aU9#xHd!q}ri4qfR+#dzmD^)#L1Xwmo$wWV(Mt zPu>pocus_EF9EG13|m+Clz+d5pT{s9tNJ``Y){Y9o}9jJcS|Je+v|xL8#6M;3lF(! znRc5tPvO-CSD)vYq`w^1(LU-byqALu4Y*X;Yj%5E0Q*Y+={0Y--QC4{?cR|Y6Sw-| zS6{ei=09(j=Q`9ovD#~+j>Xv2m&TBij0g!1eD58v?r&Nkt)-n>a+4CAE^0}6(ZJ09 z{7MFqO~}(CPhTQ~;~u{d&*0s^u9T@C#)bd-gXS2vgY+L4>qfZ4=Df?C+L5$=f3lZO ztHo!T>}2D1p2R&gGQdjhV9h#hfzaT-)Wd2Lox3Kdm<6wo?6*#j^ji9H7klPZc1Ceu zYnt2eri|&FVUA|ZcG~ol^h-?%Kc!|LnV+DE-H(a)3Ywm2P?_`SmEey?Fjxy*N9`v@ zM#qijPDYOARdlxwc737oD#x3;CFjXw`Q$^ru8cf0#ZYZS;Uz&v6ewGxq46eUrB+`s0p$=? zIh<+%vAXz`00bc#yb42LrJ;PlLlC76e26g^A7T&{TE&Bzpz<+5Q2yt_Fq8T>MEa`2 zG?2E6r*u@M?fkVF1YSJ|f>4$J^^T{r5+Vqbj$+I#1DQ110nm|(P!X8E8L(*{yP^{X07S>HI6O}*IV=`L;t>Xj;O*?T?_~cU+jI)Q delta 1821 zcmZWqdpJ~S9Nr_fnI)-dqNm0wX4fEd&dfPyW{?VF8{D%Ji&PR~wp5#Ia@j5{l1+tdBqC{R&rBup{q;NVdEfVU&i8x1_iKX&?G7$c z0VLCO`8-EQN08AxkcdF52$4W2l1pS^0!5@u%7+L6o+Vg>E7c5L~V;KoEr|Ks0?W5x@#gngBB)SB@*J(g1u6n_w~+*aVe1fd=8N zK73K|#A}wyOOFgN3MG280RUBPnSlb#!n_$I0ALqh*H0km!H{aTF^LMplYD4g7Z+zi zj7SKwanu+5WCzn#N@FvCrTWKdB{*>R1i$KOD}C^$|FU}E9jjxodu(WM zU}>wS|!JhMf6dMkmB}X#5_O$r>=n9)1|lSvebU<#Ur4_<$zlpp>H!?uQ15rO!VD5 zsp3A6n^H_k&AK-dl}8tBO}J;+F9@@mfskeD$q*q~D__7CMd@f?Hd-9#weX--`$`iJ}?dYdB)j&kw>gT2=G7KCimhjVRTz8qVwz=2MxH0FJ<+~4 zA_+Es^n6x6M`^5iVN^k>tq)^13L*j9AMpI*rzod&Z8gElnkUcPCzu z=A69dvCVP!-hJQ+=WiP#b~ zXD=zOBc?68HD~D{dWU_&yN&M(5)$l%@#%3rj$~PR{CIg^@8h`9 zypXJhH)?P5w3Q9!Zy%0zjg5{Cw+-H$f9=07*Kwn+YY*R)V1c#CLVO=BER6N(c%rI* zh;klXd$K>oor_GW5J?p=Ak3JA`UvDW2*TRj(K7LK{xP(4*n4Yjih4u}V?aE9P}Qjg zc)qAQ7MPQ?dGgVpD;3K?T|-p_i40(>RFWN@A;ly}31K)G%?EfjQF01cUC=HxY!?tFeV>j(qY^Uqk>jbo`bb>rTNNa`1G{*gKiEq gzsPMO5M~kZImRdiaz&h6BqpFJolT%nINomm12}MyT>t<8 diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png new file mode 100644 index 0000000000000000000000000000000000000000..baf0736c4b40da2e1d40423323336105181ee147 GIT binary patch literal 38486 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 diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png deleted file mode 100644 index 9de9152228c4f6800eb10e2778ec4ffcf1b6cb09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5449 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 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift index 09cc1de5f5..8adadf8b13 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift @@ -7,6 +7,7 @@ import Display import AccountContext import ChatControllerInteraction import LegacyMediaPickerUI +import MediaPickerUI extension ChatControllerImpl { func openGifEditing(file: FileMediaReference, addCaption: Bool) { @@ -28,6 +29,9 @@ extension ChatControllerImpl { getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false, hasTimer: false) }, + photoToolbarView: { [context = self.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) + }, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == self.context.account.peerId, diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 4b66aecd5c..1ee89a7565 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -168,12 +168,8 @@ extension ChatControllerImpl { allReactionsAreAvailable = false } - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - if premiumConfiguration.isPremiumDisabled { - allReactionsAreAvailable = false - } - if allReactionsAreAvailable { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) actions.getEmojiContent = { [weak self] animationCache, animationRenderer in guard let self else { preconditionFailure() @@ -188,7 +184,7 @@ extension ChatControllerImpl { hasTrending: false, topReactionItems: reactionItems, areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, + areCustomEmojiEnabled: !premiumConfiguration.isPremiumDisabled, chatPeerId: self.chatLocation.peerId, selectedItems: selectedReactions.files ) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift index 7ce1b07481..ec1ea41250 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift @@ -7,6 +7,7 @@ import Display import ContextUI import UndoUI import AccountContext +import ChatMessageBubbleContentNode import ChatMessageItemView import ChatMessageItemCommon import ChatControllerInteraction @@ -42,18 +43,28 @@ extension ChatControllerImpl { if let peerId = pollOption.addedBy { addedByPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) } - + + let messageItemView = params.messageNode as? ChatMessageItemView + let associatedData = (params.messageNode as? ChatMessageBubbleContentNode)?.item?.associatedData ?? messageItemView?.item?.associatedData + let _ = combineLatest( queue: Queue.mainQueue(), - contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView), + contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: messageItemView), addedByPeer ).start(next: { [weak self] actions, addedByPeer in guard let self else { return } - + var items: [ContextMenuItem] = [] - if !poll.isClosed && (selectedOptions.isEmpty || !poll.revotingDisabled) { + + let isRestricted = associatedData?.isPollVotingRestricted( + poll: poll, + accountTestingEnvironment: self.context.account.testingEnvironment, + currentTimestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + ) ?? false + let canVote = poll.pollId.namespace == Namespaces.Media.CloudPoll && !Namespaces.Message.allNonRegular.contains(message.id.namespace) && !isPollEffectivelyClosed(message: EngineMessage(message), poll: poll) && !isRestricted && (selectedOptions.isEmpty || !poll.revotingDisabled) + if canVote { if selectedOptions.contains(pollOption.opaqueIdentifier) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Poll_RetractOptionVote, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unvote"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in guard let self else { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index b78a817208..1c6b716c02 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -268,7 +268,7 @@ func openWebAppImpl( } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: "", url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: "", url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botId, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -317,11 +317,13 @@ func openWebAppImpl( } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: hasWebApp ? "" : nil, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: hasWebApp ? "" : nil, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botPeer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: chatPeer?.id ?? botPeer.id, botAddress: botPeer.addressName ?? "", query: query, chatTypes: chatTypes, completion: completion) }, completion: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl { parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -730,7 +732,7 @@ public extension ChatControllerImpl { updateProgress() }) |> deliverOnMainQueue).startStandalone(next: { [weak parentController, weak chatController] result in - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, botAddress: botPeer.addressName ?? "", appName: botApp.shortName, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, botAddress: botPeer.addressName ?? "", appName: botApp.shortName, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) var presentImpl: ((ViewController, Any?) -> Void)? let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: chatController, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d1ee4cda89..9ccb490700 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -572,7 +572,7 @@ func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubj var suggestedPeers: [EnginePeer] = [] if let message = await selfController.context.engine.data.get( TelegramEngine.EngineData.Item.Messages.Message(id: replySubject.messageId) - ).get(), case let .user(author) = message.author { + ).get(), case let .user(author) = message.author, author.id != selfController.context.account.peerId { suggestedPeers.append(.user(author)) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5667a26b96..4a6d25522c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1595,6 +1595,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transitionCompletion() }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) + }, photoToolbarView: { [context = self.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == self.context.account.peerId, presentSchedulePicker: { [weak self] _, done in guard let self else { return @@ -2235,7 +2237,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } - }, sendMessage: { [weak self] text in + }, sendMessage: { [weak self] text, sourceMessageId in guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else { return } @@ -2263,12 +2265,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel let peerId = strongSelf.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = strongSelf.chatDisplayNode.interactiveEmojis, interactiveEmojis.emojis.contains(text) { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { - strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in guard let strongSelf = self else { @@ -3297,7 +3302,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) - }, shareCurrentLocation: { [weak self] in + }, shareCurrentLocation: { [weak self] sourceMessageId in if let strongSelf = self { if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { return @@ -3312,7 +3317,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -3321,7 +3329,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })]), in: .window(.root)) } - }, shareAccountContact: { [weak self] in + }, shareAccountContact: { [weak self] sourceMessageId in if let strongSelf = self { if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { return @@ -3343,7 +3351,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).startStandalone(next: { peer in if case let .user(user) = peer, let phone = user.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }) } @@ -3672,6 +3683,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .none } } + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, case .quickReplyMessageInput = customChatContents.kind { + return .none + } if canReplyInChat(strongSelf.presentationInterfaceState, accountPeerId: strongSelf.context.account.peerId) { return .reply @@ -4436,12 +4450,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) }) - }, openPollCreation: { [weak self] isQuiz in + }, openPollCreation: { [weak self] sourceMessageId, isQuiz in guard let self else { return } let _ = self.presentVoiceMessageDiscardAlert(action: { - if let controller = self.configurePollCreation(isQuiz: isQuiz) { + if let controller = self.configurePollCreation(sourceMessageId: sourceMessageId, isQuiz: isQuiz) { controller.navigationPresentation = .modal self.effectiveNavigationController?.pushViewController(controller) } @@ -4663,6 +4677,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText legacyMediaEditor(context: strongSelf.context, peer: EnginePeer(peer), threadTitle: strongSelf.contentData?.state.threadInfo?.title, media: mediaReference, mode: .draw, initialCaption: inputText, snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: true) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, sendMessagesWithSignals: { [weak self] signals, _, _, _ in if let strongSelf = self { strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in }) @@ -9435,36 +9451,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser else { return } - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(context: strongSelf.context, number: phoneNumber), EnginePeer(peer).compactDisplayTitle).string)) - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - guard let strongSelf = self else { - return - } - let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id) - |> deliverOnMainQueue).startStandalone(error: { _ in - guard let strongSelf = self else { - return - } - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { - guard let strongSelf = self else { - return - } - strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root)) - }) - })) - - 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.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(context: strongSelf.context, number: phoneNumber), EnginePeer(peer).compactDisplayTitle).string, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id) + |> deliverOnMainQueue).startStandalone(error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { + guard let strongSelf = self else { + return + } + strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root)) + }) + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) }) } @@ -9721,7 +9733,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func openUrlIn(_ url: String) { - let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { self?.chatDisplayNode.dismissInput() @@ -9729,7 +9741,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) + self.push(actionSheet) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift index ae365afad0..adcaf3a277 100644 --- a/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift @@ -16,14 +16,7 @@ extension ChatControllerImpl { guard let self, let itemNode else { return } - - #if DEBUG - var value = value - if "".isEmpty { - value = 0 - } - #endif - + if value >= 2 { return } @@ -33,7 +26,7 @@ extension ChatControllerImpl { } Queue.mainQueue().after(0.5) { - let sourceRect = sourceNode.view.convert(sourceNode.view.bounds, to: nil).offsetBy(dx: -35.0, dy: 0.0) + let sourceRect = sourceNode.view.convert(sourceNode.view.bounds, to: nil) self.messageTooltipController?.dismiss() self.guestChatMessageTooltipController?.dismiss() @@ -55,16 +48,7 @@ extension ChatControllerImpl { strongSelf.guestChatMessageTooltipController = nil } } - - // let _ = self.chatDisplayNode.messageTransitionNode.addCustomOffsetHandler(itemNode: itemNode, update: { [weak tooltipScreen] offset, transition in - // guard let tooltipScreen, tooltipScreen.isNodeLoaded else { - // return false - // } - // tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition) - // - // return true - // }) - + self.present(tooltipScreen, in: .current) let _ = ApplicationSpecificNotice.incrementGuestChatMessageTooltip(accountManager: self.context.sharedContext.accountManager).startStandalone() diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 65bd61e2bf..90a77455d6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -20,7 +20,6 @@ import MediaPickerUI import LegacyCamera import LegacyMediaPickerUI import LocationUI -import WebSearchUI import WebUI import UndoUI import ICloudResources @@ -1076,14 +1075,13 @@ extension ChatControllerImpl { } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }) } }, openFileGallery: { self?.presentFileMediaPickerOptions(editingMessage: true) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + }, openWebSearch: { }, openMap: { self?.presentLocationPicker() }, openContacts: { @@ -1495,32 +1493,7 @@ extension ChatControllerImpl { legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting, scheduleTime in - if let legacyController = legacyController { - legacyController.dismiss() - } - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { signals in - if let strongSelf = self { - if editingMedia { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - })) - controller.getCaptionPanelView = { [weak self] in - return self?.getCaptionPanelView(isFile: fileMode) - } - strongSelf.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { + configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: nil, presentSelectionLimitExceeded: { guard let strongSelf = self else { return } @@ -1572,120 +1545,6 @@ extension ChatControllerImpl { }) } - func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in - self?.attachmentController?.dismiss(animated: true, completion: nil) - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak self] signals in - if let strongSelf = self, !signals.isEmpty { - if editingMessage { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - }), activateOnDisplay: activateOnDisplay) - controller.attemptItemSelection = { [weak strongSelf] item in - guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return false - } - - enum ItemType { - case gif - case image - case video - } - - var itemType: ItemType? - switch item { - case let .internalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - case let .externalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - var bannedSendGifs: (Int32, Bool)? - - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - if let value = channel.hasBannedPermission(.banSendGifs) { - bannedSendGifs = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendGifs) { - bannedSendGifs = (Int32.max, false) - } - } - - if let itemType { - switch itemType { - case .image: - if bannedSendPhotos != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .video: - if bannedSendVideos != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .gif: - if bannedSendGifs != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - } - } - - return true - } - controller.getCaptionPanelView = { [weak strongSelf] in - return strongSelf?.getCaptionPanelView(isFile: false) - } - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } - func presentLocationPicker() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -1704,7 +1563,7 @@ extension ChatControllerImpl { return } let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in + let controller = LocationPickerController(context: strongSelf.context, style: .glass, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in guard let strongSelf = self else { return } @@ -1987,6 +1846,8 @@ extension ChatControllerImpl { } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, dismissedWithResult: { [weak self] in self?.attachmentController?.dismiss(animated: false, completion: nil) }, finishedTransitionIn: { [weak self] in @@ -2083,7 +1944,7 @@ extension ChatControllerImpl { self.push(mainController) } - func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { + func configurePollCreation(sourceMessageId: EngineMessage.Id? = nil, isQuiz: Bool? = nil) -> ViewController? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil } @@ -2100,7 +1961,9 @@ extension ChatControllerImpl { guard let self else { return } - let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } ?? self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let self { self.chatDisplayNode.collapseInput() @@ -2149,7 +2012,7 @@ extension ChatControllerImpl { correlationId: nil, bubbleUpEmojiOrStickersets: [] ) - self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) + self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject)]) }) } ) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 30de107397..472a29425e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -496,11 +496,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if case .standard(.embedded) = chatPresentationInterfaceState.mode { isEmbeddedMode = true } - + if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject, case .hashTagSearch = customChatContents.kind { isEmbeddedMode = true } - + + let isSharedMediaPolls: Bool + if isEmbeddedMode, case let .tag(tag)? = chatPresentationInterfaceState.subject, tag == .polls { + isSharedMediaPolls = true + } else { + isSharedMediaPolls = false + } + var hasExpandedAudioTranscription = false if let messageNode = messageNode as? ChatMessageBubbleItemNode { hasExpandedAudioTranscription = messageNode.hasExpandedAudioTranscription() @@ -941,9 +948,36 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState |> deliverOnMainQueue |> map { data, updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer -> ContextController.Items in let isPremium = accountPeer?.isPremium ?? false - + var actions: [ContextMenuItem] = [] - + + if isSharedMediaPolls && messages.count == 1 { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.SharedMedia_ViewInChat, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + c?.dismiss(completion: { + guard let navigationController = controllerInteraction.navigationController() else { + return + } + guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else { + return + } + if case let .channel(channel) = peer, channel.isForumOrMonoForum, let threadId = message.threadId { + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone() + } else { + let targetLocation: NavigateToChatControllerParams.Location + if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { + targetLocation = .replyThread(replyThreadMessage) + } else { + targetLocation = .peer(peer) + } + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true)) + } + }) + }))) + } + var isPinnedMessages = false if case .pinnedMessages = chatPresentationInterfaceState.subject { isPinnedMessages = true diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift index fe69bffc7d..47ac7716c8 100644 --- a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift @@ -106,7 +106,18 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme - self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: []) + self.closeButton.setImage(generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(interfaceState.theme.chat.inputPanel.panelControlColor.cgColor) + context.setLineWidth(1.33) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + }), for: []) self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } @@ -115,14 +126,14 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { let contentRightInset: CGFloat = 14.0 + rightInset let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width - 3.0, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) - self.buttonTitle.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RequestsToJoin(self.count), font: Font.regular(16.0), textColor: interfaceState.theme.rootController.navigationBar.accentTextColor) + self.buttonTitle.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RequestsToJoin(self.count), font: Font.medium(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlColor) transition.updateFrame(node: self.button, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) let titleSize = self.buttonTitle.updateLayout(CGSize(width: width - leftInset - 90.0 - contentRightInset, height: 100.0)) - var buttonTitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - titleSize.width) * 0.5), y: floor((panelHeight - titleSize.height) * 0.5)), size: titleSize) + var buttonTitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - titleSize.width) * 0.5), y: floor((panelHeight - titleSize.height) * 0.5) + 1.0), size: titleSize) buttonTitleFrame.origin.x = max(buttonTitleFrame.minX, leftInset + 90.0) transition.updatePosition(node: self.buttonTitle, position: buttonTitleFrame.origin) self.buttonTitle.bounds = CGRect(origin: CGPoint(), size: buttonTitleFrame.size) @@ -132,7 +143,7 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { if let avatarsContent = self.avatarsContent { let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) - transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: leftInset + 8.0, y: floor((panelHeight - avatarsSize.height) / 2.0)), size: avatarsSize)) + transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: leftInset + 4.0, y: floor((panelHeight - avatarsSize.height) / 2.0)), size: avatarsSize)) } self.activateAreaNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight)) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 61744d861e..b4bc3caa51 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -934,7 +934,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let button = attribute.rows[0].buttons[0] switch button.action { case .text: - controllerInteraction.sendMessage(button.title) + controllerInteraction.sendMessage(button.title, message.id) return case let .url(url): var isConcealed = true @@ -944,10 +944,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise())) return case .requestMap: - controllerInteraction.shareCurrentLocation() + controllerInteraction.shareCurrentLocation(message.id) return case .requestPhone: - controllerInteraction.shareAccountContact() + controllerInteraction.shareAccountContact(message.id) return case .openWebApp: let progressPromise = Promise() diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index d272c478ae..6c958947a2 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -280,7 +280,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } let inset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { inset = 0.0 diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index 3345411ed1..a8deff185a 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -13,7 +13,6 @@ import AlertUI import PresentationDataUtils import LegacyUI import ItemListAvatarAndNameInfoItem -import WebSearchUI import PeerInfoUI import MapResourceToAvatarSizes import LegacyMediaPickerUI @@ -493,7 +492,7 @@ public func createChannelController(context: AccountContext, mode: CreateChannel keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) var dismissPickerImpl: (() -> Void)? - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: .channel, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { clearPendingAvatar() }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in avatarPickerHolder = nil diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index 14ecb0c14b..5acbb90d51 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -20,7 +20,6 @@ import LegacyUI import LocationUI import ItemListPeerItem import ItemListAvatarAndNameInfoItem -import WebSearchUI import Geocoding import PeerInfoUI import MapResourceToAvatarSizes @@ -923,7 +922,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) var dismissPickerImpl: (() -> Void)? - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: .group, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { clearPendingAvatar() }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in avatarPickerHolder = nil @@ -1043,32 +1042,32 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, changeLocation: { endEditingImpl?() - let controller = LocationPickerController(context: context, mode: .pick, completion: { location, _, _, address, _ in - let addressSignal: Signal - if let address = address { - addressSignal = .single(address) - } else { - addressSignal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) - |> map { placemark in - if let placemark = placemark { - return placemark.fullAddress - } else { - return "\(location.latitude), \(location.longitude)" - } - } - } - - let _ = (addressSignal - |> deliverOnMainQueue).start(next: { address in - addressPromise.set(.single(address)) - updateState { current in - var current = current - current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address) - return current - } - }) - }) - pushImpl?(controller) + let controller = LocationPickerController(context: context, style: .glass, mode: .pick, completion: { location, _, _, address, _ in + let addressSignal: Signal + if let address = address { + addressSignal = .single(address) + } else { + addressSignal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) + |> map { placemark in + if let placemark = placemark { + return placemark.fullAddress + } else { + return "\(location.latitude), \(location.longitude)" + } + } + } + + let _ = (addressSignal + |> deliverOnMainQueue).start(next: { address in + addressPromise.set(.single(address)) + updateState { current in + var current = current + current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address) + return current + } + }) + }) + pushImpl?(controller) }, updateWithVenue: { venue in guard let venueData = venue.venue else { return diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index e02fd3fa31..16bec6dad9 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -82,6 +82,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, ASScrollVie self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index d448a26b79..7ed2e227bf 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1944,6 +1944,8 @@ func openResolvedUrlImpl( browserIdentifier = "duckDuckGo" } else if browser.hasPrefix("Alook") { browserIdentifier = "alook" + } else if browser.hasPrefix("Vivaldi") { + browserIdentifier = "vivaldi" } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 8684af27ad..685b93753a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1014,7 +1014,16 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let host = parsedUrl.host, telegramMeHosts.contains(host) { continueHandling() } else { - if isTelegraPhLink(parsedUrl.absoluteString) { + if isTelegramWebShortLink(parsedUrl.absoluteString) { + handleInternetUrl( + parsedUrl: parsedUrl, + originalUrl: url, + context: context, + presentationData: presentationData, + navigationController: navigationController, + handleInternalUrl: handleInternalUrl + ) + } else if isTelegraPhLink(parsedUrl.absoluteString) { continueHandling() } else { context.sharedContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 7dcccfc6ba..e23bad464c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -141,7 +141,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -155,8 +155,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in @@ -203,7 +203,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift index 80dd636401..b40f5419d0 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift @@ -828,8 +828,14 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { } func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, maxHeight: CGFloat, savedMusic: Bool?, transition: ContainedViewLayoutTransition) -> CGFloat { + let previousWidth = self.validLayout?.width self.validLayout = (width, leftInset, rightInset, bottomInset, maxHeight, savedMusic) + var transition = transition + if previousWidth == nil || previousWidth?.isZero == true { + transition = .immediate + } + let finalPanelHeight = OverlayAudioPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, maxHeight: maxHeight, isExpanded: self.isExpanded, savedMusic: savedMusic) let panelHeight = OverlayAudioPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, maxHeight: maxHeight, isExpanded: self.isExpanded, savedMusic: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 2ed0591885..6b701ebe86 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2379,7 +2379,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, - sendMessage: { _ in }, + sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, @@ -2394,8 +2394,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { openUrl: { _ in }, openExternalInstantPage: { _ in }, - shareCurrentLocation: {}, - shareAccountContact: {}, + shareCurrentLocation: { _ in }, + shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, @@ -2474,7 +2474,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openMessagePollResults: { _, _ in }, - openPollCreation: { _ in + openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, @@ -3967,8 +3967,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) } - public func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) { - return avatarMediaPickerController(context: context, getSourceRect: getSourceRect, canDelete: canDelete, performDelete: performDelete, completion: completion, dismissed: dismissed) + public func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) { + return avatarMediaPickerController(context: context, peerType: peerType, getSourceRect: getSourceRect, canDelete: canDelete, performDelete: performDelete, completion: completion, dismissed: dismissed) } public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController { diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index d89f341efa..97de6187fc 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -1133,7 +1133,9 @@ public final class SharedWakeupManager { } private func updateAccounts(hasTasks: Bool, endTaskAfterTransactionsComplete: UIBackgroundTaskIdentifier?) { - if self.inForeground || self.hasActiveAudioSession || self.isInBackgroundExtension || self.backgroundProcessingTaskId != nil || self.backgroundStoryProcessingTaskId != nil || (hasTasks && self.currentExternalCompletion != nil) || self.activeExplicitExtensionTimer != nil || self.silenceAudioRenderer != nil { + let hasBackgroundLocationTask = self.accountsAndTasks.contains(where: { $0.2.backgroundLocation }) + + if self.inForeground || self.hasActiveAudioSession || self.isInBackgroundExtension || self.backgroundProcessingTaskId != nil || self.backgroundStoryProcessingTaskId != nil || hasBackgroundLocationTask || (hasTasks && self.currentExternalCompletion != nil) || self.activeExplicitExtensionTimer != nil || self.silenceAudioRenderer != nil { Logger.shared.log("Wakeup", "enableBeginTransactions: true (active)") for (account, primary, tasks) in self.accountsAndTasks { @@ -1144,7 +1146,7 @@ public final class SharedWakeupManager { } else { account.shouldBeServiceTaskMaster.set(.single(.never)) } - account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0)) + account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.backgroundLocation || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0)) account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground)) account.shouldKeepBackgroundDownloadConnections.set(.single(tasks.backgroundDownloads)) } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 55d1c95193..8961fa49e0 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -111,8 +111,6 @@ public struct ApplicationSpecificItemCacheCollectionId { } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { - case webSearchRecentQueries = 0 - case wallpaperSearchRecentQueries = 1 case localThemes = 3 case storyDrafts = 4 case storySources = 5 @@ -122,8 +120,6 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { } public struct ApplicationSpecificOrderedItemListCollectionId { - public static let webSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.webSearchRecentQueries.rawValue) - public static let wallpaperSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.wallpaperSearchRecentQueries.rawValue) public static let settingsSearchRecentItems = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.settingsSearchRecentItems.rawValue) public static let localThemes = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.localThemes.rawValue) public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue) diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 9766d60e4b..5a92356531 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -8,7 +8,15 @@ import TelegramUIPreferences import TelegramNotices import AccountContext -private let baseTelegramMePaths = ["telegram.me", "t.me", "telegram.dog"] +private let baseTelegramMePaths = [ + "telegram.me", + "t.me", "telegram.dog" +] +private let telegramWebShortLinkHosts = [ + "a.t.me", + "k.t.me", + "z.t.me" +] private let baseTelegraPhPaths = [ "telegra.ph/", "te.legra.ph/", @@ -18,6 +26,20 @@ private let baseTelegraPhPaths = [ "telegram.org/tour/" ] +public func isTelegramWebShortLink(_ url: String) -> Bool { + var urlWithScheme = url + if !urlWithScheme.contains("://") { + urlWithScheme = "https://\(urlWithScheme)" + } + guard let parsedUrl = URL(string: urlWithScheme), let host = parsedUrl.host?.lowercased() else { + return false + } + if let scheme = parsedUrl.scheme?.lowercased(), scheme != "http" && scheme != "https" { + return false + } + return telegramWebShortLinkHosts.contains(host) +} + extension ResolvedBotAdminRights { init?(_ string: String) { var rawValue: UInt32 = 0 @@ -1548,6 +1570,10 @@ public func resolveUrlImpl(context: AccountContext, peerId: EnginePeer.Id?, url: } } + if isTelegramWebShortLink(url) { + return .single(.result(.externalUrl(url))) + } + for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" diff --git a/submodules/Weather/BUILD b/submodules/Weather/BUILD new file mode 100644 index 0000000000..20aba18a37 --- /dev/null +++ b/submodules/Weather/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Weather", + module_name = "Weather", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AccountContext:AccountContext", + "//submodules/DeviceAccess", + "//submodules/DeviceLocationManager", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramCore:TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift b/submodules/Weather/Sources/Weather.swift similarity index 76% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift rename to submodules/Weather/Sources/Weather.swift index f6f93ba274..e6f6f9360a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift +++ b/submodules/Weather/Sources/Weather.swift @@ -2,17 +2,69 @@ import Foundation import CoreLocation import SwiftSignalKit import TelegramCore -import StickerPickerScreen import AccountContext import DeviceLocationManager import DeviceAccess -struct StoryWeather { - let emoji: String - let temperature: Double +public struct WeatherData { + public let emoji: String + public let temperature: Double + + public init(emoji: String, temperature: Double) { + self.emoji = emoji + self.temperature = temperature + } } -private func getWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { +public enum WeatherDataState { + case none + case notDetermined + case notAllowed + case notPreloaded + case fetching + case loaded(WeatherData) +} + +public func getWeatherData(context: AccountContext, load: Bool) -> Signal { + if !load { + return .single(.notPreloaded) + } + + guard let locationManager = context.sharedContext.locationManager else { + return .single(.none) + } + + return DeviceAccess.authorizationStatus(subject: .location(.send)) + |> mapToSignal { status in + switch status { + case .notDetermined: + return .single(.notDetermined) + case .denied, .restricted, .unreachable, .limited: + return .single(.notAllowed) + case .allowed: + return .single(.fetching) + |> then( + currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) + |> mapToSignal { location in + guard let location else { + return .single(.none) + } + + return requestWeatherData(context: context, location: location) + |> map { weather in + if let weather { + return .loaded(weather) + } else { + return .none + } + } + } + ) + } + } +} + +public func requestWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { let appConfiguration = context.currentAppConfiguration.with { $0 } let botConfiguration = WeatherBotConfiguration.with(appConfiguration: appConfiguration) @@ -20,7 +72,7 @@ private func getWeatherData(context: AccountContext, location: CLLocationCoordin return context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { - return .complete() + return .single(nil) } return .single(result) } @@ -36,65 +88,18 @@ private func getWeatherData(context: AccountContext, location: CLLocationCoordin return .single(nil) } } - |> map { contextResult -> StoryWeather? in + |> map { contextResult -> WeatherData? in guard let contextResult, let result = contextResult.results.first, let emoji = result.title, let temperature = result.description.flatMap(Double.init) else { return nil } - return StoryWeather(emoji: emoji, temperature: temperature) + let effectiveEmoji = emojiFor(for: strippedEmoji(emoji), date: Date(), location: location) + return WeatherData(emoji: effectiveEmoji, temperature: temperature) } } else { return .single(nil) } } -func getWeather(context: AccountContext, load: Bool) -> Signal { - guard let locationManager = context.sharedContext.locationManager else { - return .single(.none) - } - - return DeviceAccess.authorizationStatus(subject: .location(.send)) - |> mapToSignal { status in - switch status { - case .notDetermined: - return .single(.notDetermined) - case .denied, .restricted, .unreachable, .limited: - return .single(.notAllowed) - case .allowed: - if load { - return .single(.fetching) - |> then( - currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) - |> mapToSignal { location in - if let location { - return getWeatherData(context: context, location: location) - |> mapToSignal { weather in - if let weather { - let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) - if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { - return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( - emoji: effectiveEmoji, - emojiFile: match.file._parse(), - temperature: weather.temperature - ))) - } else { - return .single(.none) - } - } else { - return .single(.none) - } - } - } else { - return .single(.none) - } - } - ) - } else { - return .single(.notPreloaded) - } - } - } -} - private struct WeatherBotConfiguration { static var defaultValue: WeatherBotConfiguration { return WeatherBotConfiguration(botName: "izweatherbot") @@ -118,6 +123,16 @@ private struct WeatherBotConfiguration { private let J1970: Double = 2440588.0 private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"] +private func strippedEmoji(_ emoji: String) -> String { + var string = "" + for scalar in emoji.unicodeScalars { + if scalar.value != 0xfe0f { + string.unicodeScalars.append(scalar) + } + } + return string +} + private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String { var emoji = emoji if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) { diff --git a/submodules/WebSearchUI/BUILD b/submodules/WebSearchUI/BUILD deleted file mode 100644 index 7bde4f23ba..0000000000 --- a/submodules/WebSearchUI/BUILD +++ /dev/null @@ -1,39 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "WebSearchUI", - module_name = "WebSearchUI", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/TelegramCore:TelegramCore", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/AccountContext:AccountContext", - "//submodules/LegacyComponents:LegacyComponents", - "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/MergeLists:MergeLists", - "//submodules/GalleryUI:GalleryUI", - "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", - "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", - "//submodules/CheckNode:CheckNode", - "//submodules/PhotoResources:PhotoResources", - "//submodules/SearchBarNode:SearchBarNode", - "//submodules/ItemListUI:ItemListUI", - "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", - "//submodules/SegmentedControlNode:SegmentedControlNode", - "//submodules/AppBundle:AppBundle", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/AttachmentUI:AttachmentUI", - "//submodules/RadialStatusNode:RadialStatusNode", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift deleted file mode 100644 index b26bbdb913..0000000000 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import UIKit -import LegacyComponents -import SwiftSignalKit -import TelegramCore -import SSignalKit -import Display -import TelegramPresentationData -import AccountContext -import LegacyUI -import LegacyMediaPickerUI - -func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) { - guard let item = legacyWebSearchItem(engine: context.engine, result: result) else { - return - } - - var screenImage: Signal = .single(nil) - if let resource = item.thumbnailResource { - screenImage = context.engine.resources.data(resource: EngineMediaResource(resource), attemptSynchronously: true) - |> map { maybeData -> UIImage? in - if maybeData.isComplete { - if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []), let image = UIImage(data: loadedData) { - return image - } - } - return nil - } - } - - let _ = (screenImage - |> take(1) - |> deliverOnMainQueue).start(next: { screenImage in - let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout) - legacyController.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style - - let paintStickersContext = LegacyPaintStickersContext(context: context) - - let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: screenImage ?? UIImage(), availableTabs: TGPhotoEditorController.defaultTabs(forAvatarIntent: true), selectedTab: .cropTab)! - controller.stickersContext = paintStickersContext - legacyController.bind(controller: controller) - - controller.editingContext = TGMediaEditingContext() - 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 - return item.thumbnailImageSignal() - } - controller.requestOriginalScreenSizeImage = { _, position -> SSignal in - return item.screenImageSignal(position) - } - controller.requestOriginalFullSizeImage = { _, position -> SSignal in - return item.originalImageSignal(position) - } - - let fromView = transitionView(result)! - let transition = TGMediaAvatarEditorTransition(controller: controller, from: fromView)! - transition.transitionHostView = transitionHostView() - transition.referenceFrame = { - return fromView.frame - } - transition.referenceImageSize = { - return item.dimensions - } - transition.referenceScreenImageSignal = { - return item.screenImageSignal(0.0) - } - transition.imageReady = { - updateHiddenMedia(result.id) - } - - controller.beginCustomTransitionOut = { [weak legacyController] outFrame, outView, completion in - transition.outReferenceFrame = outFrame - transition.repView = outView - transition.dismiss(animated: true, completion: { - updateHiddenMedia(nil) - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - legacyController?.dismiss() - }) - } - - present(legacyController, nil) - transition.present(animated: true) - }) -} diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift deleted file mode 100644 index ac7aee4ab9..0000000000 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ /dev/null @@ -1,474 +0,0 @@ -import Foundation -import UIKit -import LegacyComponents -import SwiftSignalKit -import TelegramCore -import SSignalKit -import UIKit -import Display -import TelegramPresentationData -import AccountContext -import PhotoResources -import LegacyUI -import LegacyMediaPickerUI - -class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem { - var isVideo: Bool { - return false - } - - var uniqueIdentifier: String! { - return self.result.id - } - - let result: ChatContextResult - private(set) var thumbnailResource: TelegramMediaResource? - private(set) var imageResource: TelegramMediaResource? - let dimensions: CGSize - let thumbnailImage: Signal - let originalImage: Signal - let progress: Signal - - init(result: ChatContextResult) { - self.result = result - self.dimensions = CGSize() - self.thumbnailImage = .complete() - self.originalImage = .complete() - self.progress = .complete() - } - - init(result: ChatContextResult, thumbnailResource: TelegramMediaResource?, imageResource: TelegramMediaResource?, dimensions: CGSize, thumbnailImage: Signal, originalImage: Signal, progress: Signal) { - self.result = result - self.thumbnailResource = thumbnailResource - self.imageResource = imageResource - self.dimensions = dimensions - self.thumbnailImage = thumbnailImage - self.originalImage = originalImage - self.progress = progress - } - - var originalSize: CGSize { - return self.dimensions - } - - func thumbnailImageSignal() -> SSignal! { - return SSignal(generator: { subscriber -> SDisposable? in - let disposable = self.thumbnailImage.start(next: { image in - subscriber.putNext(image) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - } - - func screenImageAndProgressSignal() -> SSignal { - return SSignal { subscriber in - let imageDisposable = self.originalImage.start(next: { image in - if !image.degraded() { - subscriber.putNext(1.0) - } - subscriber.putNext(image) - if !image.degraded() { - subscriber.putCompletion() - } - }) - - let progressDisposable = (self.progress - |> deliverOnMainQueue).start(next: { next in - subscriber.putNext(next) - }) - - return SBlockDisposable { - imageDisposable.dispose() - progressDisposable.dispose() - } - } - } - - func screenImageSignal(_ position: TimeInterval) -> SSignal! { - return self.originalImageSignal(position) - } - - func originalImageSignal(_ position: TimeInterval) -> SSignal! { - return SSignal(generator: { subscriber -> SDisposable? in - let disposable = self.originalImage.start(next: { image in - subscriber.putNext(image) - if !image.degraded() { - subscriber.putCompletion() - } - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - } -} - -private class LegacyWebSearchGalleryItem: TGModernGalleryImageItem, TGModernGalleryEditableItem, TGModernGallerySelectableItem { - var selectionContext: TGMediaSelectionContext! - var editingContext: TGMediaEditingContext! - var stickersContext: TGPhotoPaintStickersContext! - let item: LegacyWebSearchItem - - init(item: LegacyWebSearchItem) { - self.item = item - super.init() - } - - func editableMediaItem() -> TGMediaEditableItem! { - return self.item - } - - func selectableMediaItem() -> TGMediaSelectableItem! { - return self.item - } - - func toolbarTabs() -> TGPhotoEditorTab { - return [.cropTab, .paintTab, .toolsTab] - } - - func uniqueId() -> String! { - return self.item.uniqueIdentifier - } - - override func viewClass() -> AnyClass! { - return LegacyWebSearchGalleryItemView.self - } - - override func isEqual(_ object: Any?) -> Bool { - if let item = object as? LegacyWebSearchGalleryItem { - return item.item.result.id == self.item.result.id - } - return false - } -} - -private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView { - private let readyForTransition = SVariable() - - @objc func setHiddenAsBeingEdited(_ hidden: Bool) { - self.imageView.isHidden = hidden - } - - @objc func singleTap() { - if let item = item as? LegacyWebSearchGalleryItem, let selectionContext = item.selectionContext { - selectionContext.toggleItemSelection(item.selectableMediaItem(), success: nil) - } - } - - override func readyForTransitionIn() -> SSignal! { - return self.readyForTransition.signal().take(1) - } - - override func setItem(_ item: TGModernGalleryItem!, synchronously: Bool) { - if let item = item as? LegacyWebSearchGalleryItem { - self._setItem(item) - self.imageSize = TGFitSize(item.editableMediaItem().originalSize!, CGSize(width: 1600, height: 1600)) - - let signal = item.editingContext.imageSignal(for: item.editableMediaItem())?.map(toSignal: { result -> SSignal in - if let image = result as? UIImage { - return SSignal.single(image) - } else if result == nil, let mediaItem = item.editableMediaItem() as? LegacyWebSearchItem { - return mediaItem.screenImageAndProgressSignal() - } else { - return SSignal.complete() - } - }) - - self.imageView.setSignal(signal?.deliver(on: SQueue.main()).afterNext({ [weak self] next in - if let strongSelf = self, let image = next as? UIImage { - strongSelf.imageSize = image.size - strongSelf.reset() - strongSelf.readyForTransition.set(SSignal.single(true)) - } - })) - - self.reset() - } else { - self.imageView.setSignal(nil) - super.setItem(item, synchronously: synchronously) - } - } - - override func contentView() -> UIView! { - return self.imageView - } - - override func transitionContentView() -> UIView! { - return self.contentView() - } - - override func transitionViewContentRect() -> CGRect { - let contentView = self.transitionContentView()! - return contentView.convert(contentView.bounds, to: self.transitionView()) - } -} - -func legacyWebSearchItem(engine: TelegramEngine, result: ChatContextResult) -> LegacyWebSearchItem? { - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions = CGSize() - var immediateThumbnailData: Data? - - let thumbnailSignal: Signal - let originalSignal: Signal - - switch result { - case let .externalReference(externalReference): - if let content = externalReference.content { - imageResource = content.resource - } - if let thumbnail = externalReference.thumbnail { - thumbnailResource = thumbnail.resource - thumbnailDimensions = thumbnail.dimensions?.cgSize - } - if let dimensions = externalReference.content?.dimensions { - imageDimensions = dimensions.cgSize - } - case let .internalReference(internalReference): - immediateThumbnailData = internalReference.image?.immediateThumbnailData - if let image = internalReference.image { - if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { - imageDimensions = imageRepresentation.dimensions.cgSize - imageResource = imageRepresentation.resource - } - if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100)) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } - - if let imageResource = imageResource { - let progressSignal = engine.resources.status(resource: EngineMediaResource(imageResource)) - |> map { status -> Float in - switch status { - case .Local: - return 1.0 - case .Remote, .Paused: - return 0.027 - case let .Fetching(_, progress): - return max(progress, 0.1) - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - 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, isPersonal: false)) - let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - thumbnailSignal = chatMessagePhotoDatas(postbox: engine.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) { - return .single(image) - } else { - return .complete() - } - } - originalSignal = chatMessagePhotoDatas(postbox: engine.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) - |> mapToSignal { value -> Signal in - let thumbnailData = value._0 - let fullSizeData = value._1 - let fullSizeComplete = value._3 - - if fullSizeComplete, let data = fullSizeData, let image = UIImage(data: data) { - return .single(image) - } else if let data = thumbnailData, let image = UIImage(data: data) { - image.setDegraded(true) - return .single(image) - } else { - return .complete() - } - } - - return LegacyWebSearchItem(result: result, thumbnailResource: thumbnailResource, imageResource: imageResource, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal, progress: progressSignal) - } else { - return nil - } -} - -private func galleryItems(engine: TelegramEngine, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { - var focusItem: TGModernGalleryItem? - var galleryItems: [TGModernGalleryItem] = [] - for result in results { - if let item = legacyWebSearchItem(engine: engine, result: result) { - let galleryItem = LegacyWebSearchGalleryItem(item: item) - galleryItem.selectionContext = selectionContext - galleryItem.editingContext = editingContext - if result.id == current.id { - focusItem = galleryItem - } - galleryItems.append(galleryItem) - } - } - 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, 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 - - let recipientName: String? - if let threadTitle { - recipientName = threadTitle - } else { - if peer?.id == context.account.peerId { - recipientName = presentationData.strings.DialogList_SavedMessages - } else { - recipientName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - } - } - - let paintStickersContext = LegacyPaintStickersContext(context: context) - paintStickersContext.captionPanelView = { - return getCaptionPanelView() - } - - let controller = TGModernGalleryController(context: legacyController.context)! - controller.asyncTransitionIn = true - legacyController.bind(controller: controller) - - let (items, focusItem) = galleryItems(engine: context.engine, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) - - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false, hasCoverButton: false)! - model.stickersContext = paintStickersContext - controller.model = model - model.controller = controller - model.useGalleryImageAsEditableItemImage = true - model.storeOriginalImageForItem = { item, image in - editingContext.setOriginalImage(image, for: item, synchronous: false) - } - model.willFinishEditingItem = { item, adjustments, representation, hasChanges in - if hasChanges { - editingContext.setAdjustments(adjustments, for: item) - } - editingContext.setTemporaryRep(representation, for: item) - if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem { - selectionContext.setItem(item, selected: true) - } - } - model.didFinishEditingItem = { item, adjustments, result, thumbnail in - editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: true) - } - model.saveItemCaption = { item, caption in - editingContext.setCaption(caption, for: item) - if let selectionContext = selectionContext, let caption = caption, caption.length > 0, let item = item as? TGMediaSelectableItem { - selectionContext.setItem(item, selected: true) - } - } - if let selectionContext = selectionContext { - model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false) - } - model.interfaceView.donePressed = { item in - if let item = item as? LegacyWebSearchGalleryItem { - controller.dismissWhenReady(animated: true) - completed(item.item.result) - } - } - controller.transitionHost = { - return transitionHostView() - } - var transitionedIn = false - controller.itemFocused = { item in - if let item = item as? LegacyWebSearchGalleryItem, transitionedIn { - updateHiddenMedia(item.item.result.id) - } - } - controller.beginTransitionIn = { item, _ in - if let item = item as? LegacyWebSearchGalleryItem { - return transitionView(item.item.result) - } else { - return nil - } - } - controller.startedTransitionIn = { - transitionedIn = true - updateHiddenMedia(current.id) - } - controller.beginTransitionOut = { item, _ in - if let item = item as? LegacyWebSearchGalleryItem { - return transitionView(item.item.result) - } else { - return nil - } - } - controller.completedTransitionOut = { [weak legacyController] in - updateHiddenMedia(nil) - legacyController?.dismiss() - } - present(legacyController, nil) -} - -public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionContext, _ editingState: TGMediaEditingContext, enqueueChatContextResult: (ChatContextResult) -> Void, enqueueMediaMessages: ([Any]) -> Void) -{ - var results: [ChatContextResult] = [] - for item in selectionState.selectedItems() { - if let item = item as? LegacyWebSearchItem { - results.append(item.result) - } - } - - if !results.isEmpty { - var signals: [Any] = [] - for result in results { - let editableItem = LegacyWebSearchItem(result: result) - if let adjustments = editingState.adjustments(for: editableItem) { - 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 { - var dict: [AnyHashable: Any] = [ - "type": "editedPhoto", - "image": image - ] - - if animated { - dict["isAnimation"] = true - if let photoEditorValues = adjustments as? PGPhotoEditorValues { - dict["adjustments"] = TGVideoEditAdjustments(photoEditorValues: photoEditorValues, preset: TGMediaVideoConversionPresetAnimation) - } - - let filePath = NSTemporaryDirectory().appending("/gifvideo_\(arc4random()).jpg") - let data = image.jpegData(compressionQuality: 0.8) - if let data = data { - let _ = try? data.write(to: URL(fileURLWithPath: filePath), options: []) - } - dict["url"] = NSURL(fileURLWithPath: filePath) - - if adjustments.cropApplied(forAvatar: false) || adjustments.hasPainting() || adjustments.toolsApplied() { - var paintingImage: UIImage? = adjustments.paintingData?.stillImage - if paintingImage == nil { - paintingImage = adjustments.paintingData?.image - } - - let thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(image.size, CGSize(width: 512.0, height: 512.0)), adjustments.originalSize, true, true, true, false) - if let thumbnailImage = thumbnailImage { - dict["previewImage"] = thumbnailImage - } - } - } - - return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) as Any - } else { - return SSignal.complete() - } - } - signals.append(signal as Any) - } - } else { - enqueueChatContextResult(result) - } - } - - if !signals.isEmpty { - enqueueMediaMessages(signals) - } - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift b/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift deleted file mode 100644 index c52090e332..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramPresentationData - -final class WebSearchBadgeNode: ASDisplayNode { - private var fillColor: UIColor - private var strokeColor: UIColor - private var textColor: UIColor - - private let textNode: ASTextNode - private let backgroundNode: ASImageNode - - private let font: UIFont = Font.with(size: 17.0, design: .round, weight: .bold) - - var text: String = "" { - didSet { - self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) - self.invalidateCalculatedLayout() - } - } - - convenience init(theme: PresentationTheme) { - self.init(fillColor: theme.list.itemCheckColors.fillColor, strokeColor: theme.list.itemCheckColors.fillColor, textColor: theme.list.itemCheckColors.foregroundColor) - } - - init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { - self.fillColor = fillColor - self.strokeColor = strokeColor - self.textColor = textColor - - self.textNode = ASTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textNode) - } - - func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { - self.fillColor = fillColor - self.strokeColor = strokeColor - self.textColor = textColor - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) - self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) - } - - func animateBump(incremented: Bool) { - if incremented { - let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2) - firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in - if finished { - let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) - secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) - } - }) - } else { - let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8) - firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in - if finished { - let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) - secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) - } - }) - } - } - - func animateOut() { - let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue - self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) - self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let badgeSize = self.textNode.measure(constrainedSize) - let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 12.0), height: 22.0) - let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) - self.backgroundNode.frame = backgroundFrame - self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize) - - return backgroundSize - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift deleted file mode 100644 index d82b94462d..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ /dev/null @@ -1,608 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Display -import AsyncDisplayKit -import TelegramCore -import LegacyComponents -import TelegramUIPreferences -import TelegramPresentationData -import AccountContext -import AttachmentUI - -public enum WebSearchMode { - case media - case avatar -} - -public enum WebSearchControllerMode { - case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool, Int32?) -> Void) - case editor(completion: (UIImage) -> Void) - case avatar(initialQuery: String?, completion: (UIImage) -> Void) - - var mode: WebSearchMode { - switch self { - case .media, .editor: - return .media - case .avatar: - return .avatar - } - } -} - -final class WebSearchControllerInteraction { - let openResult: (ChatContextResult) -> Void - let setSearchQuery: (String) -> Void - let deleteRecentQuery: (String) -> Void - let toggleSelection: (ChatContextResult, Bool) -> Bool - let sendSelected: (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void - let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void - let avatarCompleted: (UIImage) -> Void - let selectionState: TGMediaSelectionContext? - let editingState: TGMediaEditingContext - var hiddenMediaId: String? - - init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Bool, sendSelected: @escaping (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { - self.openResult = openResult - self.setSearchQuery = setSearchQuery - self.deleteRecentQuery = deleteRecentQuery - self.toggleSelection = toggleSelection - self.sendSelected = sendSelected - self.schedule = schedule - self.avatarCompleted = avatarCompleted - self.selectionState = selectionState - self.editingState = editingState - } -} - -private func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal { - return Signal { subscriber in - let disposable = selectionState.selectionChangedSignal()?.start(next: { next in - subscriber.putNext(Void()) - }, completed: {}) - return ActionDisposable { - disposable?.dispose() - } - } -} - -public struct WebSearchConfiguration: Equatable { - public let gifProvider: String? - - public init(appConfiguration: AppConfiguration) { - var gifProvider: String? = nil - if let data = appConfiguration.data, let value = data["gif_search_branding"] as? String { - gifProvider = value - } - self.gifProvider = gifProvider - } -} - -public final class WebSearchController: ViewController { - private var validLayout: ContainerViewLayout? - - private let context: AccountContext - let mode: WebSearchControllerMode - private let peer: EnginePeer? - private let chatLocation: ChatLocation? - private let configuration: EngineConfiguration.SearchBots - private let activateOnDisplay: Bool - - private var controllerNode: WebSearchControllerNode { - return self.displayNode as! WebSearchControllerNode - } - - private var _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private var didPlayPresentationAnimation = false - - private var controllerInteraction: WebSearchControllerInteraction? - private var interfaceState: WebSearchInterfaceState - private let interfaceStatePromise = ValuePromise() - - private var disposable: Disposable? - private let resultsDisposable = MetaDisposable() - private var selectionDisposable: Disposable? - - private var navigationContentNode: WebSearchNavigationContentNode? - - public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } { - didSet { - self.controllerNode.getCaptionPanelView = self.getCaptionPanelView - } - } - - public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } - - public var dismissed: () -> Void = { } - - public var searchingUpdated: (Bool) -> Void = { _ in } - - public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true } - - private var searchQueryPromise = ValuePromise() - private var searchQueryDisposable: Disposable? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) { - self.context = context - self.mode = mode - self.peer = peer - self.chatLocation = chatLocation - self.configuration = configuration - self.activateOnDisplay = activateOnDisplay - - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - self.interfaceState = WebSearchInterfaceState(presentationData: presentationData) - - var searchQuery: String? - if case let .avatar(initialQuery, _) = mode, let query = initialQuery { - searchQuery = query - self.interfaceState = self.interfaceState.withUpdatedQuery(query) - } - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.list.plainBackgroundColor).withUpdatedBackgroundColor(presentationData.theme.list.plainBackgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) - self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style - - self.scrollToTop = { [weak self] in - if let strongSelf = self { - strongSelf.controllerNode.scrollToTop(animated: true) - } - } - - let settings = self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webSearchSettings]) - |> map { sharedData -> WebSearchSettings in - if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webSearchSettings]?.get(WebSearchSettings.self) { - return current - } else { - return WebSearchSettings.defaultSettings - } - } - - let gifProvider = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) - |> map { preferencesEntry -> String? in - guard let appConfiguration = preferencesEntry?.get(AppConfiguration.self) else { - return nil - } - let configuration = WebSearchConfiguration(appConfiguration: appConfiguration) - return configuration.gifProvider - } - |> distinctUntilChanged - - self.disposable = ((combineLatest(settings, (updatedPresentationData?.signal ?? context.sharedContext.presentationData), gifProvider)) - |> deliverOnMainQueue).start(next: { [weak self] settings, presentationData, gifProvider in - guard let strongSelf = self else { - return - } - strongSelf.updateInterfaceState { current -> WebSearchInterfaceState in - var updated = current - if case .media = mode, current.state?.scope != settings.scope { - updated = updated.withUpdatedScope(settings.scope) - } - if current.presentationData !== presentationData { - updated = updated.withUpdatedPresentationData(presentationData) - } - if current.gifProvider != gifProvider { - updated = updated.withUpdatedGifProvider(gifProvider) - } - return updated - } - }) - - var attachment = false - if case let .media(attachmentValue, _) = mode { - attachment = attachmentValue - } else if case .editor = mode { - attachment = true - } - let navigationContentNode = WebSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, attachment: attachment) - self.navigationContentNode = navigationContentNode - navigationContentNode.setQueryUpdated { [weak self] query in - if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.searchQueryPromise.set(query) - strongSelf.searchingUpdated(!query.isEmpty) - } - } - navigationContentNode.cancel = { [weak self] in - if let strongSelf = self { - strongSelf.cancel() - } - } - self.navigationBar?.setContentNode(navigationContentNode, animated: false) - if let query = searchQuery { - navigationContentNode.setQuery(query) - } - - let selectionState: TGMediaSelectionContext? - switch self.mode { - case .media: - selectionState = TGMediaSelectionContext() - case .avatar: - selectionState = nil - case .editor: - selectionState = nil - } - let editingState = TGMediaEditingContext() - self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerNode.openResult(currentResult: result, present: { [weak self] viewController, arguments in - if let strongSelf = self { - strongSelf.present(viewController, in: .window(.root), with: arguments, blockInteraction: true) - } - }) - } - }, setSearchQuery: { [weak self] query in - if let strongSelf = self { - strongSelf.navigationContentNode?.setQuery(query) - strongSelf.updateSearchQuery(query) - strongSelf.navigationContentNode?.deactivate() - } - }, deleteRecentQuery: { [weak self] query in - if let strongSelf = self { - let _ = removeRecentWebSearchQuery(engine: strongSelf.context.engine, string: query).start() - } - }, toggleSelection: { [weak self] result, value in - if let strongSelf = self { - if !strongSelf.attemptItemSelection(result) { - return false - } - let item = LegacyWebSearchItem(result: result) - strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value) - return true - } else { - return false - } - }, sendSelected: { [weak self] current, silently, scheduleTime, messageEffect in - if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults { - if let current = current { - let currentItem = LegacyWebSearchItem(result: current) - selectionState.setItem(currentItem, selected: true) - } - if case let .media(_, sendSelected) = mode { - sendSelected(results, selectionState, editingState, silently, scheduleTime) - } - } - }, schedule: { [weak self] messageEffect in - if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in - self?.controllerInteraction?.sendSelected(nil, silentPosting, time, nil) - }) - } - }, avatarCompleted: { result in - if case let .avatar(_, avatarCompleted) = mode { - avatarCompleted(result) - } - }, selectionState: selectionState, editingState: editingState) - - selectionState?.attemptSelectingItem = { [weak self] item in - guard let self else { - return false - } - - if let item = item as? LegacyWebSearchItem { - return self.attemptItemSelection(item.result) - } - - return true - } - - if let selectionState = selectionState { - self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState) - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - strongSelf.controllerNode.updateSelectionState(animated: true) - } - }) - } - - let throttledSearchQuery = self.searchQueryPromise.get() - |> mapToSignal { query -> Signal in - if !query.isEmpty { - return (.complete() |> delay(1.0, queue: Queue.mainQueue())) - |> then(.single(query)) - } else { - return .single(query) - } - } - - self.searchQueryDisposable = (throttledSearchQuery - |> deliverOnMainQueue).start(next: { [weak self] query in - if let self { - self.updateSearchQuery(query) - } - }) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable?.dispose() - self.resultsDisposable.dispose() - self.selectionDisposable?.dispose() - self.searchQueryDisposable?.dispose() - } - - public func cancel() { - self.controllerNode.dismissInput?() - self.controllerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in - self?.dismissed() - self?.dismiss() - }) - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - if case .modalSheet = presentationArguments.presentationAnimation { - self.controllerNode.animateIn() - } - } - } - - private var didActivateSearch = false - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - var select = false - if case let .avatar(initialQuery, _) = self.mode, let _ = initialQuery { - select = true - } - if case let .media(attachment, _) = self.mode, attachment && !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } else if case .editor = self.mode, !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - if !self.didActivateSearch && self.activateOnDisplay { - self.didActivateSearch = true - self.navigationContentNode?.activate(select: select) - } - } - - override public func loadDisplayNode() { - var attachment: Bool = false - if case let .media(attachmentValue, _) = self.mode, attachmentValue { - attachment = true - } - self.displayNode = WebSearchControllerNode(controller: self, context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment) - self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in - if let strongSelf = self { - strongSelf.updateInterfaceState(f) - } - } - self.controllerNode.cancel = { [weak self] in - if let strongSelf = self { - strongSelf.dismiss() - } - } - self.controllerNode.dismissInput = { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.deactivate() - } - } - self.controllerNode.updateInterfaceState(self.interfaceState, animated: false) - - self._ready.set(.single(true)) - self.displayNodeDidLoad() - } - - private func updateInterfaceState(animated: Bool = true, _ f: (WebSearchInterfaceState) -> WebSearchInterfaceState) { - let previousInterfaceState = self.interfaceState - let previousTheme = self.interfaceState.presentationData.theme - let previousStrings = self.interfaceState.presentationData.theme - let previousGifProvider = self.interfaceState.gifProvider - - let updatedInterfaceState = f(self.interfaceState) - self.interfaceState = updatedInterfaceState - self.interfaceStatePromise.set(updatedInterfaceState) - - if self.isNodeLoaded { - if previousTheme !== updatedInterfaceState.presentationData.theme || previousStrings !== updatedInterfaceState.presentationData.strings || previousGifProvider != updatedInterfaceState.gifProvider { - self.controllerNode.updatePresentationData(theme: updatedInterfaceState.presentationData.theme, strings: updatedInterfaceState.presentationData.strings) - } - if previousInterfaceState != self.interfaceState { - self.controllerNode.updateInterfaceState(self.interfaceState, animated: animated) - } - } - } - - private func updateSearchQuery(_ query: String) { - let scope: Signal - switch self.mode { - case .media: - scope = self.interfaceStatePromise.get() - |> map { state -> WebSearchScope? in - return state.state?.scope - } - |> distinctUntilChanged - case .avatar, .editor: - scope = .single(.images) - } - - self.updateInterfaceState { $0.withUpdatedQuery(query) } - - let scopes: [WebSearchScope: Promise<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool)>] = [.images: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .images) - |> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in - return .single((result, false)) - |> then(.single((result, true))) - }), .gifs: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .gifs) - |> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in - return .single((result, false)) - |> then(.single((result, true))) - })] - - var results = scope - |> mapToSignal { scope -> (Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError>) in - if let scope = scope, let scopeResults = scopes[scope] { - return scopeResults.get() - } else { - return .complete() - } - } - - if query.isEmpty { - results = .single(({ _ in return nil}, false)) - self.navigationContentNode?.setActivity(false) - } - - let previousResults = Atomic<(ChatContextResultCollection, Bool)?>(value: nil) - self.resultsDisposable.set((results - |> deliverOnMainQueue).start(next: { [weak self] result, immediate in - if let strongSelf = self { - if let result = result(nil), case let .contextRequestResult(_, results) = result { - if let results = results { - let previous = previousResults.swap((results, immediate)) - if let previous = previous, previous.0.queryId == results.queryId && !previous.1 { - } else { - strongSelf.controllerNode.updateResults(results, immediate: immediate) - } - } - } else { - strongSelf.controllerNode.updateResults(nil) - } - } - })) - } - - private func signalForQuery(_ query: String, scope: WebSearchScope) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> { - let delayRequest = true - let signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return .contextRequestResult(nil, nil) }) - - let peerId = self.peer?.id ?? self.context.account.peerId - - let botName: String? - switch scope { - case .images: - botName = self.configuration.imageBotUsername - case .gifs: - botName = self.configuration.gifBotUsername - } - guard let name = botName else { - return .single({ _ in return .contextRequestResult(nil, nil) }) - } - - let context = self.context - let contextBot = self.context.engine.peers.resolvePeerByName(name: name, referrer: nil) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, 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: peerId, limit: 64) - |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in - return .contextRequestResult(.user(user), results?.results) - } - } - - let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in - var passthroughPreviousResult: ChatContextResultCollection? - if let previousResult = previousResult { - if case let .contextRequestResult(previousUser, previousResults) = previousResult { - if previousUser?.id == user.id { - passthroughPreviousResult = previousResults - } - } - } - return .contextRequestResult(nil, passthroughPreviousResult) - }) - - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> - if delayRequest { - maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) - } else { - maybeDelayedContextResults = results - } - - return botResult |> then(maybeDelayedContextResults) - } else { - return .single({ _ in return nil }) - } - } - return (signal |> then(contextBot)) - |> deliverOnMainQueue - |> beforeStarted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(true) - } - } - |> afterCompleted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(false) - } - } - } - - public var mediaPickerContext: WebSearchPickerContext? { - if let interaction = self.controllerInteraction { - return WebSearchPickerContext(interaction: interaction) - } else { - return nil - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.validLayout = layout - - let navigationBarHeight = self.navigationLayout(layout: layout).navigationFrame.maxY - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - } -} - -public class WebSearchPickerContext: AttachmentMediaPickerContext { - private weak var interaction: WebSearchControllerInteraction? - - public var selectionCount: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in - subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0)) - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - public var caption: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in - if let caption = caption as? NSAttributedString { - subscriber.putNext(caption) - } else { - subscriber.putNext(nil) - } - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - init(interaction: WebSearchControllerInteraction) { - self.interaction = interaction - } - - public func setCaption(_ caption: NSAttributedString) { - self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true) - } - - public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.interaction?.sendSelected(nil, mode == .silently, nil, parameters) - } - - public func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.interaction?.schedule(parameters) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift deleted file mode 100644 index 0aabc3e62a..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ /dev/null @@ -1,829 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit -import Display -import TelegramCore -import LegacyComponents -import TelegramPresentationData -import TelegramUIPreferences -import MergeLists -import AccountContext -import GalleryUI -import ChatListSearchItemHeader -import SegmentedControlNode -import AppBundle - -private struct WebSearchContextResultStableId: Hashable { - let result: ChatContextResult - - func hash(into hasher: inout Hasher) { - hasher.combine(result.id) - } - - static func ==(lhs: WebSearchContextResultStableId, rhs: WebSearchContextResultStableId) -> Bool { - return lhs.result == rhs.result - } -} - -private struct WebSearchEntry: Comparable, Identifiable { - let index: Int - let result: ChatContextResult - - var stableId: WebSearchContextResultStableId { - return WebSearchContextResultStableId(result: self.result) - } - - static func ==(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool { - return lhs.index == rhs.index && lhs.result == rhs.result - } - - static func <(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> GridItem { - return WebSearchItem(account: account, theme: theme, interfaceState: interfaceState, result: self.result, controllerInteraction: controllerInteraction) - } -} - -private struct WebSearchTransition { - let deleteItems: [Int] - let insertItems: [GridNodeInsertItem] - let updateItems: [GridNodeUpdateItem] - let entryCount: Int - let hasMore: Bool -} - -private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries: [WebSearchEntry], hasMore: Bool, account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> WebSearchTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction)) } - - return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore) -} - -private func gridNodeLayoutForContainerLayout(_ layout: ContainerViewLayout) -> GridNodeLayoutType { - let itemsPerRow: Int - if case .compact = layout.metrics.widthClass { - switch layout.orientation { - case .portrait: - itemsPerRow = 3 - case .landscape: - itemsPerRow = 5 - } - } else { - itemsPerRow = 3 - } - - let side = floorToScreenPixels((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) - return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0) -} - - -private struct WebSearchRecentQueryStableId: Hashable { - let query: String -} - -private struct WebSearchRecentQueryEntry: Comparable, Identifiable { - let index: Int - let query: String - - var stableId: WebSearchRecentQueryStableId { - return WebSearchRecentQueryStableId(query: self.query) - } - - static func ==(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool { - return lhs.index == rhs.index && lhs.query == rhs.query - } - - static func <(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> ListViewItem { - return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: self.query, tapped: { query in - controllerInteraction.setSearchQuery(query) - }, deleted: { query in - controllerInteraction.deleteRecentQuery(query) - }, header: header) - } -} - -private struct WebSearchRecentTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecentQueryEntry], to toEntries: [WebSearchRecentQueryEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> WebSearchRecentTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) } - - return WebSearchRecentTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -class WebSearchControllerNode: ASDisplayNode { - private weak var controller: WebSearchController? - private let context: AccountContext - private let peer: EnginePeer? - private let chatLocation: ChatLocation? - private var theme: PresentationTheme - private var strings: PresentationStrings - private var presentationData: PresentationData - private let mode: WebSearchMode - private let attachment: Bool - - private let controllerInteraction: WebSearchControllerInteraction - private var webSearchInterfaceState: WebSearchInterfaceState - private let webSearchInterfaceStatePromise: ValuePromise - - private let segmentedContainerNode: ASDisplayNode - private let segmentedBackgroundNode: ASDisplayNode - private let segmentedSeparatorNode: ASDisplayNode - private let segmentedControlNode: SegmentedControlNode - - private let toolbarBackgroundNode: ASDisplayNode - private let toolbarSeparatorNode: ASDisplayNode - private let cancelButton: HighlightableButtonNode - private let sendButton: HighlightableButtonNode - private let badgeNode: WebSearchBadgeNode - - private let attributionNode: ASImageNode - - private let recentQueriesNode: ListView - private var enqueuedRecentTransitions: [(WebSearchRecentTransition, Bool)] = [] - - private var gridNode: GridNode - private var enqueuedTransitions: [(WebSearchTransition, Bool)] = [] - private var dequeuedInitialTransitionOnLayout = false - - private(set) var currentExternalResults: ChatContextResultCollection? - private var currentProcessedResults: ChatContextResultCollection? - private var currentEntries: [WebSearchEntry]? - private var hasMore = false - private var isLoadingMore = false - - private let hiddenMediaId = Promise(nil) - private var hiddenMediaDisposable: Disposable? - - private let results = ValuePromise(nil, ignoreRepeated: true) - - private let disposable = MetaDisposable() - private let loadMoreDisposable = MetaDisposable() - - private var recentDisposable: Disposable? - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var requestUpdateInterfaceState: (Bool, (WebSearchInterfaceState) -> WebSearchInterfaceState) -> Void = { _, _ in } - var cancel: (() -> Void)? - var dismissInput: (() -> Void)? - - var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } - - init(controller: WebSearchController, context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) { - self.controller = controller - self.context = context - self.theme = presentationData.theme - self.strings = presentationData.strings - self.presentationData = presentationData - self.controllerInteraction = controllerInteraction - self.peer = peer - self.chatLocation = chatLocation - self.mode = mode - self.attachment = attachment - - self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 }) - self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true) - - self.segmentedContainerNode = ASDisplayNode() - self.segmentedContainerNode.clipsToBounds = true - - self.segmentedBackgroundNode = ASDisplayNode() - self.segmentedSeparatorNode = ASDisplayNode() - - let items = [ - strings.WebSearch_Images, - strings.WebSearch_GIFs - ] - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: 0) - - self.toolbarBackgroundNode = ASDisplayNode() - self.toolbarSeparatorNode = ASDisplayNode() - - self.attributionNode = ASImageNode() - - self.cancelButton = HighlightableButtonNode() - self.sendButton = HighlightableButtonNode() - - self.badgeNode = WebSearchBadgeNode(theme: theme) - - self.gridNode = GridNode() - self.gridNode.backgroundColor = theme.list.plainBackgroundColor - - self.recentQueriesNode = ListViewImpl() - self.recentQueriesNode.backgroundColor = theme.list.plainBackgroundColor - self.recentQueriesNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - super.init() - - self.setViewBlock({ - return UITracingLayerView() - }) - - self.addSubnode(self.gridNode) - if !attachment { - self.addSubnode(self.recentQueriesNode) - } - self.addSubnode(self.segmentedContainerNode) - self.segmentedContainerNode.addSubnode(self.segmentedBackgroundNode) - self.segmentedContainerNode.addSubnode(self.segmentedSeparatorNode) -// if case .media = mode { -// self.segmentedContainerNode.addSubnode(self.segmentedControlNode) -// } - if !attachment { - self.addSubnode(self.toolbarBackgroundNode) - self.addSubnode(self.toolbarSeparatorNode) - self.addSubnode(self.cancelButton) - self.addSubnode(self.sendButton) - self.addSubnode(self.attributionNode) - self.addSubnode(self.badgeNode) - } - - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let strongSelf = self, let scope = WebSearchScope(rawValue: Int32(index)) { - let _ = updateWebSearchSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager) { _ -> WebSearchSettings in - return WebSearchSettings(scope: scope) - }.start() - strongSelf.requestUpdateInterfaceState(true) { current in - return current.withUpdatedScope(scope) - } - } - } - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: .touchUpInside) - - self.applyPresentationData() - - self.disposable.set((combineLatest(self.results.get(), self.webSearchInterfaceStatePromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] results, interfaceState in - if let strongSelf = self { - strongSelf.updateInternalResults(results, interfaceState: interfaceState) - } - })) - - if !attachment { - let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil) - self.recentDisposable = (combineLatest(webSearchRecentQueries(engine: self.context.engine), self.webSearchInterfaceStatePromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in - if let strongSelf = self { - var entries: [WebSearchRecentQueryEntry] = [] - for i in 0 ..< queries.count { - entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i])) - } - - let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: { _ in - let _ = clearRecentWebSearchQueries(engine: strongSelf.context.engine).start() - }) - - let previousEntries = previousRecentItems.swap(entries) - - let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header) - strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil) - } - }) - } - - self.recentQueriesNode.beganInteractiveDragging = { [weak self] _ in - self?.dismissInput?() - } - - self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { - if bottom.0 >= entries.count - 8 { - strongSelf.loadMore() - } - } - } - self.gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - - self.sendButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self, strongSelf.badgeNode.alpha > 0.0 { - if highlighted { - strongSelf.badgeNode.layer.removeAnimation(forKey: "opacity") - strongSelf.badgeNode.alpha = 0.4 - } else { - strongSelf.badgeNode.alpha = 1.0 - strongSelf.badgeNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.hiddenMediaDisposable = (self.hiddenMediaId.get() - |> deliverOnMainQueue).start(next: { [weak self] id in - if let strongSelf = self { - strongSelf.controllerInteraction.hiddenMediaId = id - - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode { - itemNode.updateHiddenMedia() - } - } - } - }) - } - - deinit { - self.disposable.dispose() - self.recentDisposable?.dispose() - self.loadMoreDisposable.dispose() - self.hiddenMediaDisposable?.dispose() - } - - func updatePresentationData(theme: PresentationTheme, strings: PresentationStrings) { - let themeUpdated = theme !== self.theme - self.theme = theme - self.strings = strings - - self.applyPresentationData(themeUpdated: themeUpdated) - } - - func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { - self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: transition) - transition.updateAlpha(node: self.segmentedBackgroundNode, alpha: alpha) - } - - func applyPresentationData(themeUpdated: Bool = true) { - self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal) - - if let selectionState = self.controllerInteraction.selectionState { - let sendEnabled = selectionState.count() > 0 - let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor - self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) - } - - if themeUpdated { - self.gridNode.backgroundColor = self.theme.list.plainBackgroundColor - self.segmentedBackgroundNode.backgroundColor = self.theme.list.plainBackgroundColor - self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) - self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor - self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - } - - let gifProviderImage: UIImage? - if let gifProvider = self.webSearchInterfaceState.gifProvider { - switch gifProvider { - case "tenor": - gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Tenor"), color: self.theme.list.itemSecondaryTextColor) - case "giphy": - gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Giphy"), color: self.theme.list.itemSecondaryTextColor) - default: - gifProviderImage = nil - } - } else { - gifProviderImage = nil - } - let previousGifProviderImage = self.attributionNode.image - self.attributionNode.image = gifProviderImage - - if previousGifProviderImage == nil, let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate) - } - } - - func animateIn(completion: (() -> Void)? = 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) - } - - func animateOut(completion: (() -> Void)? = nil) { - 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.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - completion?() - }) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.input]) - insets.top += navigationBarHeight - - let hasQuery = !(self.webSearchInterfaceState.state?.query ?? "").isEmpty - - let segmentedHeight: CGFloat = self.segmentedControlNode.supernode != nil ? 44.0 : 5.0 - let panelY: CGFloat = insets.top - UIScreenPixel - 4.0 - - transition.updateSublayerTransformOffset(layer: self.segmentedContainerNode.layer, offset: CGPoint(x: 0.0, y: !hasQuery ? -44.0 : 0.0), completion: nil) - transition.updateFrame(node: self.segmentedContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight))) - - transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: segmentedHeight))) - transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: segmentedHeight - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 10.0 * 2.0), transition: transition) - transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: 5.0), size: controlSize)) - - insets.top -= 4.0 - - let toolbarHeight: CGFloat = 44.0 - let toolbarY = layout.size.height - toolbarHeight - insets.bottom - transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: toolbarHeight + insets.bottom))) - transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - if let image = self.attributionNode.image { - self.attributionNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - image.size.width) / 2.0), y: toolbarY + floor((toolbarHeight - image.size.height) / 2.0)), size: image.size) - transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.scope == .gifs ? 1.0 : 0.0) - } - - let toolbarPadding: CGFloat = 10.0 - let cancelSize = self.cancelButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) - transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: toolbarPadding + layout.safeInsets.left, y: toolbarY), size: CGSize(width: cancelSize.width, height: toolbarHeight))) - - let sendSize = self.sendButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) - let sendFrame = CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight)) - transition.updateFrame(node: self.sendButton, frame: sendFrame) - - if let selectionState = self.controllerInteraction.selectionState { - self.sendButton.isHidden = false - - let previousSendEnabled = self.sendButton.isEnabled - let sendEnabled = selectionState.count() > 0 - self.sendButton.isEnabled = sendEnabled - if sendEnabled != previousSendEnabled { - let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor - self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) - } - - let selectedCount = selectionState.count() - let badgeText = String(selectedCount) - if selectedCount > 0 && (self.badgeNode.text != badgeText || self.badgeNode.alpha < 1.0) { - if transition.isAnimated { - var incremented = true - if let previousCount = Int(self.badgeNode.text) { - incremented = selectedCount > previousCount || self.badgeNode.alpha < 1.0 - } - self.badgeNode.animateBump(incremented: incremented) - } - self.badgeNode.text = badgeText - - let badgeSize = self.badgeNode.measure(layout.size) - transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) - transition.updateAlpha(node: self.badgeNode, alpha: 1.0) - } else if selectedCount == 0 { - if transition.isAnimated { - self.badgeNode.animateOut() - } - let badgeSize = CGSize(width: 22.0, height: 22.0) - transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) - transition.updateAlpha(node: self.badgeNode, alpha: 0.0) - } - } else { - self.sendButton.isHidden = true - } - - let previousBounds = self.gridNode.bounds - self.gridNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) - self.gridNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - - insets.top += segmentedHeight - insets.bottom += toolbarHeight - - let gridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: gridInsets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(layout)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - - self.recentQueriesNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - if !self.dequeuedInitialTransitionOnLayout { - self.dequeuedInitialTransitionOnLayout = true - self.dequeueTransition() - } - } - - func updateInterfaceState(_ interfaceState: WebSearchInterfaceState, animated: Bool) { - let previousGifProvider = self.webSearchInterfaceState.gifProvider - self.webSearchInterfaceState = interfaceState - self.webSearchInterfaceStatePromise.set(self.webSearchInterfaceState) - - if let state = interfaceState.state { - self.segmentedControlNode.selectedIndex = Int(state.scope.rawValue) - } - - if previousGifProvider != interfaceState.gifProvider { - self.applyPresentationData(themeUpdated: false) - } - - if let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) - } - } - - func updateSelectionState(animated: Bool) { - self.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode { - itemNode.updateSelectionState(animated: animated) - } - } - - if let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) - } - } - - func updateResults(_ results: ChatContextResultCollection?, immediate: Bool = false) { - if self.currentExternalResults == results { - return - } - let previousResults = self.currentExternalResults - self.currentExternalResults = results - self.currentProcessedResults = results - - self.isLoadingMore = false - self.loadMoreDisposable.set(nil) - - if immediate && previousResults?.query == results?.query && previousResults?.botId != results?.botId { - let previousNode = self.gridNode - - let gridNode = GridNode() - gridNode.backgroundColor = theme.list.plainBackgroundColor - gridNode.frame = previousNode.frame - - gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { - if bottom.0 >= entries.count - 8 { - strongSelf.loadMore() - } - } - } - gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - - if self.recentQueriesNode.supernode != nil { - self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode) - } else { - self.insertSubnode(gridNode, aboveSubnode: previousNode) - } - self.gridNode = gridNode - self.currentEntries = nil - let directionMultiplier: CGFloat - if let state = self.webSearchInterfaceState.state { - switch state.scope { - case .images: - directionMultiplier = 1.0 - case .gifs: - directionMultiplier = -1.0 - } - } else { - directionMultiplier = 1.0 - } - - previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * self.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - gridNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } else if previousResults?.botId != results?.botId || previousResults?.query != results?.query { - self.scrollToTop() - } - - self.results.set(results) - } - - func clearResults() { - self.results.set(nil) - } - - private func loadMore() { - guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, currentProcessedResults.results.count > 55, let nextOffset = currentProcessedResults.nextOffset else { - return - } - self.isLoadingMore = true - let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in - return (geoPoint.latitude, geoPoint.longitude) - } - self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) - |> deliverOnMainQueue).start(next: { [weak self] nextResults in - guard let strongSelf = self, let nextResults = nextResults else { - return - } - strongSelf.isLoadingMore = false - var results: [ChatContextResult] = [] - var existingIds = Set() - for result in currentProcessedResults.results { - results.append(result) - existingIds.insert(result.id) - } - for result in nextResults.results.results { - if !existingIds.contains(result.id) { - results.append(result) - existingIds.insert(result.id) - } - } - let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.results.queryId, nextOffset: nextResults.results.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, webView: currentProcessedResults.webView, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) - strongSelf.currentProcessedResults = mergedResults - strongSelf.results.set(mergedResults) - })) - } - - private func updateInternalResults(_ results: ChatContextResultCollection?, interfaceState: WebSearchInterfaceState) { - var entries: [WebSearchEntry] = [] - var hasMore = false - if let state = interfaceState.state, state.query.isEmpty { - } else if let results = results { - hasMore = results.nextOffset != nil - - var index = 0 - var resultIds = Set() - for result in results.results { - let entry = WebSearchEntry(index: index, result: result) - if resultIds.contains(entry.stableId) { - continue - } else { - resultIds.insert(entry.stableId) - } - entries.append(entry) - index += 1 - } - } - - let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: hasMore, account: self.context.account, theme: interfaceState.presentationData.theme, interfaceState: interfaceState, controllerInteraction: self.controllerInteraction) - self.currentEntries = entries - - self.enqueueTransition(transition, firstTime: firstTime) - } - - private func enqueueTransition(_ transition: WebSearchTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) - - if self.containerLayout != nil { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - if let state = self.webSearchInterfaceState.state { - self.recentQueriesNode.isHidden = !state.query.isEmpty - } - - self.hasMore = transition.hasMore - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: true), completion: { _ in }) - } - } - - private func enqueueRecentTransition(_ transition: WebSearchRecentTransition, firstTime: Bool) { - enqueuedRecentTransitions.append((transition, firstTime)) - - if self.containerLayout != nil { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - } - } - - private func dequeueRecentTransition() { - if let (transition, firstTime) = self.enqueuedRecentTransitions.first { - self.enqueuedRecentTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.PreferSynchronousDrawing) - } else { - options.insert(.AnimateInsertion) - } - - self.recentQueriesNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - } - - @objc private func cancelPressed() { - self.cancel?() - } - - @objc private func sendPressed() { - self.controllerInteraction.sendSelected(nil, false, nil, nil) - - self.cancel?() - } - - func scrollToTop(animated: Bool = false) { - self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated) - } - - func openResult(currentResult: ChatContextResult, present: @escaping (ViewController, Any?) -> Void) { - self.view.endEditing(true) - - if self.controllerInteraction.selectionState != nil { - if let state = self.webSearchInterfaceState.state, state.scope == .images { - if let results = self.currentProcessedResults?.results { - presentLegacyWebSearchGallery(context: self.context, peer: self.peer, threadTitle: nil, chatLocation: self.chatLocation, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in - self?.hiddenMediaId.set(.single(id)) - }, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in - return self?.gridNode.view - }, transitionView: { [weak self] result in - return self?.transitionNode(for: result)?.transitionView() - }, completed: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.sendSelected(result, false, nil, nil) - strongSelf.cancel?() - } - }, getCaptionPanelView: self.getCaptionPanelView, present: present) - } - } else { - if let results = self.currentProcessedResults?.results { - var entries: [WebSearchGalleryEntry] = [] - var centralIndex: Int = 0 - for i in 0 ..< results.count { - entries.append(WebSearchGalleryEntry(index: entries.count, result: results[i])) - if results[i] == currentResult { - centralIndex = i - } - } - - let controller = WebSearchGalleryController(context: self.context, peer: self.peer, selectionState: self.controllerInteraction.selectionState, editingState: self.controllerInteraction.editingState, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in - - }, baseNavigationController: nil, sendCurrent: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.sendSelected(result, false, nil, nil) - strongSelf.cancel?() - } - }) - self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue) - |> map { entry in - return entry?.result.id - }) - present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in - if let strongSelf = self { - var transitionNode: WebSearchItemNode? - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id { - transitionNode = itemNode - } - } - if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: (transitionNode, transitionNode.bounds, { [weak transitionNode] in - return (transitionNode?.transitionView().snapshotContentTree(unhide: true), nil) - }), addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view) - } - }) - } - } - return nil - })) - } - } - } else { - if let mode = self.controller?.mode, case let .editor(completion) = mode { - if let item = legacyWebSearchItem(engine: self.context.engine, result: currentResult) { - let _ = (item.originalImage - |> deliverOnMainQueue).start(next: { image in - if !image.degraded() { - completion(image) - } - }) - } - } else { - presentLegacyWebSearchEditor(context: self.context, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in - self?.hiddenMediaId.set(.single(id)) - }, transitionHostView: { [weak self] in - return self?.gridNode.view - }, transitionView: { [weak self] result in - return self?.transitionNode(for: result)?.transitionView() - }, completed: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.avatarCompleted(result) - strongSelf.cancel?() - } - }, present: present) - } - } - } - - private func transitionNode(for result: ChatContextResult) -> WebSearchItemNode? { - var transitionNode: WebSearchItemNode? - self.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == result.id { - transitionNode = itemNode - } - } - return transitionNode - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift deleted file mode 100644 index dcf02efb5f..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ /dev/null @@ -1,354 +0,0 @@ -import Foundation -import UIKit -import Display -import QuickLook -import SwiftSignalKit -import AsyncDisplayKit -import TelegramCore -import LegacyComponents -import TelegramPresentationData -import AccountContext -import GalleryUI -import TelegramUniversalVideoContent - -final class WebSearchGalleryControllerInteraction { - let dismiss: (Bool) -> Void - let send: (ChatContextResult) -> Void - let selectionState: TGMediaSelectionContext? - let editingState: TGMediaEditingContext - - init(dismiss: @escaping (Bool) -> Void, send: @escaping (ChatContextResult) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { - self.dismiss = dismiss - self.send = send - self.selectionState = selectionState - self.editingState = editingState - } -} - -struct WebSearchGalleryEntry: Equatable { - let index: Int - let result: ChatContextResult - - static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool { - return lhs.result == rhs.result - } - - func item(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) -> GalleryItem { - 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, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) - 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, storeAfterDownload: nil), 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), userLocation: .other, fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) - } - } - preconditionFailure() - } -} - -final class WebSearchGalleryControllerPresentationArguments { - let animated: Bool - let transitionArguments: (WebSearchGalleryEntry) -> GalleryTransitionArguments? - - init(animated: Bool = true, transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) { - self.animated = animated - self.transitionArguments = transitionArguments - } -} - -class WebSearchGalleryController: ViewController { - private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black) - - private var galleryNode: GalleryControllerNode { - return self.displayNode as! GalleryControllerNode - } - - private let context: AccountContext - private var presentationData: PresentationData - private var controllerInteraction: WebSearchGalleryControllerInteraction? - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - private var didSetReady = false - - private let disposable = MetaDisposable() - - private var entries: [WebSearchGalleryEntry] = [] - private var centralEntryIndex: Int? - - private let centralItemTitle = Promise() - private let centralItemTitleContent = Promise() - private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() - private let centralItemAttributesDisposable = DisposableSet() - - private let titleView: GalleryTitleView - - private let checkedDisposable = MetaDisposable() - private var checkNode: GalleryNavigationCheckNode? - - private let _hiddenMedia = Promise(nil) - var hiddenMedia: Signal { - return self._hiddenMedia.get() - } - - private let replaceRootController: (ViewController, Promise?) -> Void - private let baseNavigationController: NavigationController? - - init(context: AccountContext, peer: EnginePeer?, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?, sendCurrent: @escaping (ChatContextResult) -> Void) { - self.context = context - self.replaceRootController = replaceRootController - self.baseNavigationController = baseNavigationController - - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - self.titleView = GalleryTitleView(context: context, presentationData: self.presentationData) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: WebSearchGalleryController.navigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) - - self.controllerInteraction = WebSearchGalleryControllerInteraction(dismiss: { [weak self] animated in - self?.dismiss(forceAway: false) - }, send: { [weak self] current in - sendCurrent(current) - self?.dismiss(forceAway: true) - }, selectionState: selectionState, editingState: editingState) - - if let title = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) { - let recipientNode = GalleryNavigationRecipientNode(color: .white, title: title) - let leftItem = UIBarButtonItem(customDisplayNode: recipientNode) - self.navigationItem.leftBarButtonItem = leftItem - } - - let checkNode = GalleryNavigationCheckNode(theme: self.presentationData.theme) - checkNode.addTarget(target: self, action: #selector(self.checkPressed)) - let rightItem = UIBarButtonItem(customDisplayNode: checkNode) - self.navigationItem.rightBarButtonItem = rightItem - self.checkNode = checkNode - - self.statusBar.statusBarStyle = .White - - let entriesSignal: Signal<[WebSearchGalleryEntry], NoError> = .single(entries) - - self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in - if let strongSelf = self { - strongSelf.entries = entries - strongSelf.centralEntryIndex = centralIndex - if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(context: context, presentationData: strongSelf.presentationData, controllerInteraction: strongSelf.controllerInteraction) - }), centralItemIndex: centralIndex) - - let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in - strongSelf?.didSetReady = true - } - strongSelf._ready.set(ready |> map { true }) - } - } - })) - - self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in - self?.navigationItem.title = title - })) - - self.centralItemAttributesDisposable.add(self.centralItemTitleContent.get().start(next: { [weak self] titleContent in - self?.titleView.setContent(content: titleContent) - })) - - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in - self?.galleryNode.updatePresentationState({ - $0.withUpdatedFooterContentNode(footerContentNode) - }, transition: .immediate) - })) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable.dispose() - self.checkedDisposable.dispose() - self.centralItemAttributesDisposable.dispose() - } - - @objc func checkPressed() { - if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let centralItemNode = self.galleryNode.pager.centralItemNode() as? WebSearchVideoGalleryItemNode, let item = centralItemNode.item { - let legacyItem = LegacyWebSearchItem(result: item.result) - - controllerInteraction.selectionState?.setItem(legacyItem, selected: checkNode.isChecked) - } - } - - private func dismiss(forceAway: Bool, animated: Bool = true) { - var animatedOutNode = true - var animatedOutInterface = false - - let completion = { [weak self] in - if animatedOutNode && animatedOutInterface { - self?._hiddenMedia.set(.single(nil)) - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - } - - if animated { - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - if !self.entries.isEmpty { - if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { - animatedOutNode = false - centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { - animatedOutNode = true - completion() - }) - } - } - } - - self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { - animatedOutInterface = true - completion() - }) - } else { - animatedOutInterface = true - completion() - } - } - - override func loadDisplayNode() { - let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in - if let strongSelf = self { - strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true) - } - }, pushController: { _ in - }, dismissController: { [weak self] in - self?.dismiss(forceAway: true) - }, replaceRootController: { [weak self] controller, ready in - if let strongSelf = self { - strongSelf.replaceRootController(controller, ready) - } - }, editMedia: { _ in - }, controller: { [weak self] in - return self - }, currentItemNode: { [weak self] in - return self?.galleryNode.pager.centralItemNode() - }) - self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, titleView: self.titleView) - self.displayNodeDidLoad() - - self.galleryNode.statusBar = self.statusBar - self.galleryNode.navigationBar = self.navigationBar - - self.galleryNode.transitionDataForCentralItem = { [weak self] in - if let strongSelf = self { - if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { - return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) - } - } - } - return nil - } - self.galleryNode.dismiss = { [weak self] in - self?._hiddenMedia.set(.single(nil)) - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - - self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) - }), centralItemIndex: self.centralEntryIndex) - - self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in - if let strongSelf = self { - var item: WebSearchGalleryEntry? - if let index = index { - item = strongSelf.entries[index] - - if let node = strongSelf.galleryNode.pager.centralItemNode() { - strongSelf.centralItemTitle.set(node.title()) - strongSelf.centralItemTitleContent.set(node.titleContent()) - strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) - strongSelf.centralItemFooterContentNode.set(node.footerContent()) - } - - if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState, let item = item { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) - } - } - if strongSelf.didSetReady { - strongSelf._hiddenMedia.set(.single(item)) - } - } - } - - let selectionState = self.controllerInteraction?.selectionState - let selectionUpdated = Signal { subscriber in - if let selectionState = selectionState { - let disposable = selectionState.selectionChangedSignal()!.start(next: { _ in - subscriber.putNext(Void()) - }, error: { _ in }, completed: {})! - return ActionDisposable { - disposable.dispose() - } - } else { - subscriber.putCompletion() - return EmptyDisposable - } - } - self.checkedDisposable.set((selectionUpdated - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self, let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() { - let item = strongSelf.entries[centralItemNode.index] - if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: true) - } - } - })) - - let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in - self?.didSetReady = true - } - self._ready.set(ready |> map { true }) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - var nodeAnimatesItself = false - - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - self.centralItemTitle.set(centralItemNode.title()) - self.centralItemTitleContent.set(centralItemNode.titleContent()) - self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) - self.centralItemFooterContentNode.set(centralItemNode.footerContent()) - - let item = self.entries[centralItemNode.index] - if let transitionArguments = presentationArguments.transitionArguments(item) { - nodeAnimatesItself = true - centralItemNode.activateAsInitial() - - if presentationArguments.animated { - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {}) - } - - if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let selectionState = controllerInteraction.selectionState { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) - } - - self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) - } - } - - self.galleryNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false) - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift deleted file mode 100644 index 8b355c575e..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import SwiftSignalKit -import LegacyComponents -import TelegramPresentationData -import AccountContext -import GalleryUI - -final class WebSearchGalleryFooterContentNode: GalleryFooterContentNode { - private let context: AccountContext - private var theme: PresentationTheme - private var strings: PresentationStrings - - private let cancelButton: HighlightableButtonNode - private let sendButton: HighlightableButtonNode - - var cancel: (() -> Void)? - var send: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData) { - self.context = context - self.theme = presentationData.theme - self.strings = presentationData.strings - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(TGComponentsImageNamed("PhotoPickerBackIcon"), for: [.normal]) - self.sendButton = HighlightableButtonNode() - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.theme), for: [.normal]) - - super.init() - - self.addSubnode(self.cancelButton) - self.addSubnode(self.sendButton) - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) - } - - func setCaption(_ caption: String) { - - } - - override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> LayoutInfo { - let width = size.width - let panelSize: CGFloat = 49.0 - var panelHeight: CGFloat = panelSize + bottomInset - panelHeight += contentInset - - self.cancelButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) - self.sendButton.frame = CGRect(origin: CGPoint(x: width - panelSize - rightInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) - - return LayoutInfo(height: panelHeight, needsShadow: false) - } - - override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { - self.cancelButton.alpha = 1.0 - self.sendButton.alpha = 1.0 - } - - override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - self.cancelButton.alpha = 0.0 - self.sendButton.alpha = 0.0 - completion() - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func sendButtonPressed() { - self.send?() - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift b/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift deleted file mode 100644 index ddeb5e1684..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import UIKit -import TelegramPresentationData -import TelegramUIPreferences - -struct WebSearchInterfaceInnerState: Equatable { - let scope: WebSearchScope - let query: String -} - -struct WebSearchInterfaceState: Equatable { - let state: WebSearchInterfaceInnerState? - let presentationData: PresentationData - let gifProvider: String? - - init (presentationData: PresentationData) { - self.state = nil - self.presentationData = presentationData - self.gifProvider = nil - } - - init(state: WebSearchInterfaceInnerState?, presentationData: PresentationData, gifProvider: String? = nil) { - self.state = state - self.presentationData = presentationData - self.gifProvider = gifProvider - } - - func withUpdatedScope(_ scope: WebSearchScope) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: scope, query: self.state?.query ?? ""), presentationData: self.presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedQuery(_ query: String) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: self.state?.scope ?? .images, query: query), presentationData: self.presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedPresentationData(_ presentationData: PresentationData) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: self.state, presentationData: presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedGifProvider(_ gifProvider: String?) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: self.state, presentationData: self.presentationData, gifProvider: gifProvider) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift deleted file mode 100644 index cc3b7f9587..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ /dev/null @@ -1,316 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import SwiftSignalKit -import TelegramPresentationData -import CheckNode -import PhotoResources -import RadialStatusNode - -final class WebSearchItem: GridItem { - var section: GridSection? - - let account: Account - let theme: PresentationTheme - let interfaceState: WebSearchInterfaceState - let result: ChatContextResult - let controllerInteraction: WebSearchControllerInteraction - - public init(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, result: ChatContextResult, controllerInteraction: WebSearchControllerInteraction) { - self.account = account - self.theme = theme - self.result = result - self.interfaceState = interfaceState - self.controllerInteraction = controllerInteraction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = WebSearchItemNode() - node.setup(item: self, synchronousLoad: synchronousLoad) - return node - } - - func update(node: GridItemNode) { - guard let node = node as? WebSearchItemNode else { - assertionFailure() - return - } - node.setup(item: self, synchronousLoad: false) - } -} - -final class WebSearchItemNode: GridItemNode { - private let imageNodeBackground: ASDisplayNode - private let imageNode: TransformImageNode - private var checkNode: CheckNode? - private var statusNode: RadialStatusNode? - - private(set) var item: WebSearchItem? - private var currentDimensions: CGSize? - - private let fetchStatusDisposable = MetaDisposable() - private let fetchDisposable = MetaDisposable() - private var resourceStatus: EngineMediaResource.FetchStatus? - - override init() { - self.imageNodeBackground = ASDisplayNode() - self.imageNodeBackground.isLayerBacked = true - - self.imageNode = TransformImageNode() - self.imageNode.contentAnimations = [.subsequentUpdates] - self.imageNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.imageNodeBackground) - self.addSubnode(self.imageNode) - } - - deinit { - self.fetchStatusDisposable.dispose() - self.fetchDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } - self.imageNode.view.addGestureRecognizer(recognizer) - } - - func updateProgress(_ value: Float?, animated: Bool) { - if let value { - let statusNode: RadialStatusNode - if let current = self.statusNode { - statusNode = current - } else { - statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) - statusNode.isUserInteractionEnabled = false - self.addSubnode(statusNode) - self.statusNode = statusNode - } - let adjustedProgress = max(0.027, CGFloat(value)) - let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true) - statusNode.transitionToState(state) - } else if let statusNode = self.statusNode { - self.statusNode = nil - if animated { - statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - } else { - statusNode.removeFromSupernode() - } - } - } - - func setup(item: WebSearchItem, synchronousLoad: Bool) { - if self.item !== item { - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions: CGSize? - var immediateThumbnailData: Data? - - switch item.result { - case let .externalReference(externalReference): - if let content = externalReference.content, externalReference.type != "gif" { - imageResource = content.resource - } else if let thumbnail = externalReference.thumbnail { - imageResource = thumbnail.resource - } - imageDimensions = externalReference.content?.dimensions?.cgSize - case let .internalReference(internalReference): - if let image = internalReference.image { - immediateThumbnailData = image.immediateThumbnailData - if let largestRepresentation = largestImageRepresentation(image.representations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100))?.resource - if let file = internalReference.file { - if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } else { - if let thumbnailRepresentation = smallestImageRepresentation(image.representations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } else if let file = internalReference.file { - immediateThumbnailData = file.immediateThumbnailData - if let dimensions = file.dimensions { - imageDimensions = dimensions.cgSize - } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - 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, 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, userLocation: .other, photoReference: .standalone(media: tmpImage)) - } else { - updateImageSignal = .complete() - } - - if let updateImageSignal = updateImageSignal { - let editingContext = item.controllerInteraction.editingState - let editableItem = LegacyWebSearchItem(result: item.result) - let editedImageSignal = Signal { subscriber in - if let signal = editingContext.thumbnailImageSignal(for: editableItem) { - let disposable = signal.start(next: { next in - if let image = next as? UIImage { - subscriber.putNext(image) - } else { - subscriber.putNext(nil) - } - }, error: { _ in - }, completed: nil)! - - return ActionDisposable { - disposable.dispose() - } - } else { - return EmptyDisposable - } - } - let editedSignal: Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> = editedImageSignal - |> map { image in - if let image = image { - return { arguments in - guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { - return nil - } - let drawingRect = arguments.drawingRect - let imageSize = image.size - let fittedSize = imageSize.aspectFilled(arguments.boundingSize).fitted(imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - context.withFlippedContext { c in - c.setBlendMode(.copy) - if let cgImage = image.cgImage { - drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) - } - } - return context - } - } else { - return nil - } - } - - let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = editedSignal - |> mapToSignal { result in - if result != nil { - return .single(result!) - } else { - return updateImageSignal - } - } - self.imageNode.setSignal(imageSignal) - } - - self.currentDimensions = imageDimensions - if let _ = imageDimensions { - self.setNeedsLayout() - } - self.updateHiddenMedia() - } - - self.item = item - self.updateSelectionState(animated: false) - } - - func updateSelectionState(animated: Bool) { - if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState { - let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay)) - checkNode.valueChanged = { [weak self] value in - guard let self else { - return - } - if !item.controllerInteraction.toggleSelection(item.result, value) { - self.checkNode?.setSelected(false, animated: false) - } - } - self.addSubnode(checkNode) - self.checkNode = checkNode - self.setNeedsLayout() - } - - if let item = self.item { - if let selectionState = item.controllerInteraction.selectionState { - let selected = selectionState.isIdentifierSelected(item.result.id) - self.checkNode?.setSelected(selected, animated: animated) - } - } - } - - func updateHiddenMedia() { - if let item = self.item { - let wasHidden = self.isHidden - self.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id - if !self.isHidden && wasHidden { - self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } - - func transitionView() -> UIView { - let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! - view.frame = self.convert(self.bounds, to: nil) - return view - } - - override func layout() { - super.layout() - - let imageFrame = self.bounds - self.imageNode.frame = imageFrame - - if let item = self.item, let dimensions = self.currentDimensions { - let imageSize = dimensions.aspectFilled(imageFrame.size) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))() - } - - let checkSize = CGSize(width: 28.0, height: 28.0) - self.checkNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width - 2.0, y: 2.0), size: checkSize) - } - - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - guard let item = self.item else { - return - } - - switch recognizer.state { - case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - item.controllerInteraction.openResult(item.result) - default: - break - } - } - default: - break - } - } -} - diff --git a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift b/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift deleted file mode 100644 index f7af067849..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import SearchBarNode - -private let searchBarFont = Font.regular(17.0) - -final class WebSearchNavigationContentNode: NavigationBarContentNode { - private let theme: PresentationTheme - private let strings: PresentationStrings - - private let searchBar: SearchBarNode - - private var queryUpdated: ((String) -> Void)? - - var cancel: (() -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings, attachment: Bool) { - self.theme = theme - self.strings = strings - - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern, displayBackground: !attachment) - self.searchBar.hasCancelButton = attachment - self.searchBar.placeholderString = NSAttributedString(string: attachment ? strings.Attachment_SearchWeb : strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) - - super.init() - - self.addSubnode(self.searchBar) - - self.searchBar.textUpdated = { [weak self] query, _ in - self?.queryUpdated?(query) - } - self.searchBar.cancel = { [weak self] in - self?.cancel?() - } - } - - func setQueryUpdated(_ f: @escaping (String) -> Void) { - self.queryUpdated = f - } - - func setActivity(_ activity: Bool) { - self.searchBar.activity = activity - } - - func setQuery(_ query: String) { - self.searchBar.text = query - } - - override var nominalHeight: CGFloat { - return 56.0 - } - - override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { - let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0)) - self.searchBar.frame = searchBarFrame - self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) - - return size - } - - func activate(select: Bool = false) { - self.searchBar.activate() - self.searchBar.selectAll() - } - - func deactivate() { - self.searchBar.deactivate(clear: false) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift deleted file mode 100644 index 01ba32939b..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import UIKit -import TelegramCore -import SwiftSignalKit -import TelegramUIPreferences - -private struct WebSearchRecentQueryItemId { - public let rawValue: EngineMemoryBuffer - - var value: String { - return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" - } - - init(_ rawValue: EngineMemoryBuffer) { - self.rawValue = rawValue - } - - init?(_ value: String) { - if let data = value.data(using: .utf8) { - self.rawValue = EngineMemoryBuffer(data: data) - } else { - return nil - } - } -} - -public final class RecentWebSearchQueryItem: Codable { - init() { - } - - public init(from decoder: Decoder) throws { - } - - public func encode(to encoder: Encoder) throws { - } -} - -func addRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WebSearchRecentQueryItemId(string) { - return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue, item: RecentWebSearchQueryItem(), removeTailIfCountExceeds: 100) - } else { - return .complete() - } -} - -func removeRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WebSearchRecentQueryItemId(string) { - return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue) - } else { - return .complete() - } -} - -func clearRecentWebSearchQueries(engine: TelegramEngine) -> Signal { - return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries) -} - -func webSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> { - return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries)) - |> map { items -> [String] in - var result: [String] = [] - for item in items { - let value = WebSearchRecentQueryItemId(item.id).value - result.append(value) - } - return result - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift deleted file mode 100644 index c60da15196..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift +++ /dev/null @@ -1,233 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils - -private enum RevealOptionKey: Int32 { - case delete -} - -public class WebSearchRecentQueryItem: ListViewItem { - let theme: PresentationTheme - let strings: PresentationStrings - let account: Account - let query: String - let tapped: (String) -> Void - let deleted: (String) -> Void - - let header: ListViewItemHeader? - - public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, query: String, tapped: @escaping (String) -> Void, deleted: @escaping (String) -> Void, header: ListViewItemHeader) { - self.theme = theme - self.strings = strings - self.account = account - self.query = query - self.tapped = tapped - self.deleted = deleted - self.header = header - } - - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = WebSearchRecentQueryItemNode() - let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem)) - node.contentSize = nodeLayout.contentSize - node.insets = nodeLayout.insets - - completion(node, nodeApply) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? WebSearchRecentQueryItemNode { - let layout = nodeValue.asyncLayout() - async { - let (nodeLayout, apply) = layout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem)) - Queue.mainQueue().async { - completion(nodeLayout, { info in - apply().1(info) - }) - } - } - } - } - } - - public var selectable: Bool { - return true - } - - public func selected(listView: ListView) { - listView.clearHighlightAnimated(true) - self.tapped(self.query) - } -} - -class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private var textNode: TextNode? - - private var item: WebSearchRecentQueryItem? - private var layoutParams: ListViewItemLayoutParams? - - required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - super.init(layerBacked: false, rotated: false, seeThrough: false) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = self.item { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil, previousItem == nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - let _ = nodeApply() - } - } - - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { - super.setHighlighted(highlighted, at: point, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } - - func asyncLayout() -> (_ item: WebSearchRecentQueryItem, _ params: ListViewItemLayoutParams, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { - let currentItem = self.item - - let textLayout = TextNode.asyncLayout(self.textNode) - - return { [weak self] item, params, last, firstWithHeader in - - let leftInset: CGFloat = 15.0 + params.leftInset - let rightInset: CGFloat = params.rightInset - - let attributedString = NSAttributedString(string: item.query, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) - let textApply = textLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - - return (nodeLayout, { [weak self] in - var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - return (nil, { _ in - if let strongSelf = self { - strongSelf.item = item - strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - } - - let (textLayout, textApply) = textApply - let textNode = textApply() - if strongSelf.textNode == nil { - strongSelf.textNode = textNode - strongSelf.addSubnode(textNode) - } - - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((44.0 - textLayout.size.height) / 2.0)), size: textLayout.size) - textNode.frame = textFrame - - let separatorHeight = UIScreenPixel - let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) - strongSelf.separatorNode.isHidden = last - - strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.theme.list.itemSecondaryTextColor)])) - } - }) - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } - - override public func headers() -> [ListViewItemHeader]? { - if let item = self.item { - return item.header.flatMap { [$0] } - } else { - return nil - } - } - - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - super.updateRevealOffset(offset: offset, transition: transition) - - if let params = self.layoutParams, let textNode = self.textNode { - let leftInset: CGFloat = 15.0 + params.leftInset - - var textFrame = textNode.frame - textFrame.origin.x = leftInset + offset - transition.updateFrame(node: textNode, frame: textFrame) - } - } - - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - if let item = self.item { - switch option.key { - case RevealOptionKey.delete.rawValue: - item.deleted(item.query) - default: - break - } - } - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift deleted file mode 100644 index 79ccb825de..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ /dev/null @@ -1,545 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import Display -import TelegramPresentationData -import AccountContext -import RadialStatusNode -import GalleryUI -import TelegramUniversalVideoContent -import GalleryUI - -class WebSearchVideoGalleryItem: GalleryItem { - var id: AnyHashable { - return self.index - } - - let index: Int - - let context: AccountContext - let presentationData: PresentationData - let result: ChatContextResult - let content: UniversalVideoContent - let controllerInteraction: WebSearchGalleryControllerInteraction? - - init(context: AccountContext, presentationData: PresentationData, index: Int, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { - self.context = context - self.presentationData = presentationData - self.index = index - self.result = result - self.content = content - self.controllerInteraction = controllerInteraction - } - - func node(synchronous: Bool) -> GalleryItemNode { - let node = WebSearchVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) - node.setupItem(self) - return node - } - - func updateNode(node: GalleryItemNode, synchronous: Bool) { - if let node = node as? WebSearchVideoGalleryItemNode { - node.setupItem(self) - } - } - - func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return nil - } -} - -private struct FetchControls { - let fetch: () -> Void - let cancel: () -> Void -} - -final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { - private let context: AccountContext - private let strings: PresentationStrings - private let controllerInteraction: WebSearchGalleryControllerInteraction? - - fileprivate let _ready = Promise() - - private let footerContentNode: WebSearchGalleryFooterContentNode - - private var videoNode: UniversalVideoNode? - private let statusButtonNode: HighlightableButtonNode - private let statusNode: RadialStatusNode - - private var isCentral = false - private var validLayout: (ContainerViewLayout, CGFloat)? - private var didPause = false - private var isPaused = true - - private var requiresDownload = false - - var item: WebSearchVideoGalleryItem? - - private let statusDisposable = MetaDisposable() - - private let fetchDisposable = MetaDisposable() - private var fetchStatus: EngineMediaResource.FetchStatus? - private var fetchControls: FetchControls? - - var playbackCompleted: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) { - self.context = context - self.strings = presentationData.strings - self.controllerInteraction = controllerInteraction - - self.footerContentNode = WebSearchGalleryFooterContentNode(context: context, presentationData: presentationData) - - self.statusButtonNode = HighlightableButtonNode() - self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) - - super.init() - - self.statusButtonNode.addSubnode(self.statusNode) - self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside) - - self.addSubnode(self.statusButtonNode) - - self.footerContentNode.cancel = { - controllerInteraction?.dismiss(true) - } - self.footerContentNode.send = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - controllerInteraction?.send(item.result) - } - } - } - - deinit { - self.statusDisposable.dispose() - } - - @objc override func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - if recognizer.state == .ended { - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - if let item = self.item, let selectionState = item.controllerInteraction?.selectionState { - let legacyItem = legacyWebSearchItem(engine: item.context.engine, result: item.result) - selectionState.toggleItemSelection(legacyItem, success: nil) - } - case .doubleTap: - super.contentTap(recognizer) - default: - break - } - } - } - } - - override func ready() -> Signal { - return self._ready.get() - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - self.validLayout = (layout, navigationBarHeight) - - let statusDiameter: CGFloat = 50.0 - let statusFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusDiameter) / 2.0), y: floor((layout.size.height - statusDiameter) / 2.0)), size: CGSize(width: statusDiameter, height: statusDiameter)) - transition.updateFrame(node: self.statusButtonNode, frame: statusFrame) - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size)) - } - - func setupItem(_ item: WebSearchVideoGalleryItem) { - if self.item?.content.id != item.content.id { - var isAnimated = false - var mediaResource: EngineMediaResource? - if let content = item.content as? NativeVideoContent { - isAnimated = content.fileReference.media.isAnimated - mediaResource = EngineMediaResource(content.fileReference.media.resource) - } - - if let videoNode = self.videoNode { - videoNode.canAttachContent = false - videoNode.removeFromSupernode() - } - - let mediaManager = item.context.sharedContext.mediaManager - - let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) - let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) - videoNode.updateLayout(size: videoSize, transition: .immediate) - self.videoNode = videoNode - videoNode.isUserInteractionEnabled = false - videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335) - videoNode.canAttachContent = true - - self.requiresDownload = true - var mediaFileStatus: Signal = .single(nil) - if let mediaResource = mediaResource { - mediaFileStatus = item.context.engine.resources.status(resource: mediaResource) - |> map(Optional.init) - } - - self.statusDisposable.set((combineLatest(videoNode.status, mediaFileStatus) - |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in - if let strongSelf = self { - var initialBuffering = false - var isPaused = true - if let value = value { - if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero { - let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0) - if !zoomableContent.0.equalTo(videoSize) { - strongSelf.zoomableContent = (videoSize, zoomableContent.1) - strongSelf.videoNode?.updateLayout(size: videoSize, transition: .immediate) - } - } - switch value.status { - case .playing: - isPaused = false - case let .buffering(_, whilePlaying, _, _): - initialBuffering = true - isPaused = !whilePlaying - var isStreaming = false - if let fetchStatus = strongSelf.fetchStatus { - switch fetchStatus { - case .Local: - break - default: - isStreaming = true - } - } - if let content = item.content as? NativeVideoContent, !isStreaming { - initialBuffering = false - if !content.enableSound { - isPaused = false - } - } - default: - if let content = item.content as? NativeVideoContent, !content.streamVideo.enabled { - if !content.enableSound { - isPaused = false - } - } - } - } - - var fetching = false - if initialBuffering { - strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {}) - } else { - var state: RadialStatusNodeState = .none - - if let fetchStatus = fetchStatus { - if strongSelf.requiresDownload { - switch fetchStatus { - case let .Fetching(_, progress): - fetching = true - isPaused = true - state = .progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true) - default: - break - } - } - } - strongSelf.statusNode.transitionToState(state, animated: false, completion: {}) - } - - strongSelf.isPaused = isPaused - strongSelf.fetchStatus = fetchStatus - - strongSelf.statusButtonNode.isHidden = !initialBuffering && !isPaused && !fetching - } - })) - - self.zoomableContent = (videoSize, videoNode) - - videoNode.playbackCompleted = { [weak videoNode] in - Queue.mainQueue().async { - if !isAnimated { - videoNode?.seek(0.0) - } - } - } - - self._ready.set(videoNode.ready) - } - - self.item = item - } - - override func centralityUpdated(isCentral: Bool) { - super.centralityUpdated(isCentral: isCentral) - - if self.isCentral != isCentral { - self.isCentral = isCentral - - if let videoNode = self.videoNode, videoNode.ownsContentNode { - if isCentral { - videoNode.play() - } else { - videoNode.pause() - } - } - } - } - - override func activateAsInitial() { - if self.isCentral { - self.videoNode?.play() - } - } - - override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - return - } - - if let node = node.0 as? OverlayMediaItemNode { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) - } else { - var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) - let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - let surfaceCopyView = node.2().0! - let copyView = node.2().0! - - addToTransitionSurface(surfaceCopyView) - - var transformedSurfaceFrame: CGRect? - var transformedSurfaceFinalFrame: CGRect? - if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) - transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) - } - - if let transformedSurfaceFrame = transformedSurfaceFrame { - surfaceCopyView.frame = transformedSurfaceFrame - } - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) - - surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in - copyView?.removeFromSuperview() - }) - let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - - if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in - surfaceCopyView?.removeFromSuperview() - }) - let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height) - surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - } - - videoNode.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in - videoNode?.allowsGroupOpacity = false - }) - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - } - } - - override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - - let copyView = node.2().0! - let surfaceCopyView = node.2().0! - - addToTransitionSurface(surfaceCopyView) - - var transformedSurfaceFrame: CGRect? - var transformedSurfaceCopyViewInitialFrame: CGRect? - if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) - transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) - } - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in - if positionCompleted && boundsCompleted && copyCompleted { - copyView?.removeFromSuperview() - surfaceCopyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) - surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height) - surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - } - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.allowsGroupOpacity = false - }) - - self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - } - - func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - var nodeCompleted = false - - let copyView = node.view.snapshotContentTree()! - - videoNode.isHidden = true - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - - let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) - node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - nodeCompleted = true - intermediateCompletion() - }) - } - - @objc func statusButtonPressed() { - if let videoNode = self.videoNode { - if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { - self.toggleControlsVisibility() - } - - if let fetchStatus = self.fetchStatus { - switch fetchStatus { - case .Local: - videoNode.togglePlayPause() - case .Remote, .Paused: - if self.requiresDownload { - self.fetchControls?.fetch() - } else { - videoNode.togglePlayPause() - } - case .Fetching: - self.fetchControls?.cancel() - } - } else { - videoNode.togglePlayPause() - } - } - } - - override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, nil)) - } -} diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 2b5ec4be74..47ee5b384d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -79,6 +79,7 @@ public struct WebAppParameters { let forceHasSettings: Bool let fullSize: Bool let isFullscreen: Bool + let sameOrigin: Bool let appSettings: BotAppSettings? public init( @@ -97,6 +98,7 @@ public struct WebAppParameters { forceHasSettings: Bool, fullSize: Bool, isFullscreen: Bool = false, + sameOrigin: Bool = false, appSettings: BotAppSettings? = nil ) { self.source = source @@ -114,6 +116,7 @@ public struct WebAppParameters { self.forceHasSettings = forceHasSettings self.fullSize = fullSize || isFullscreen self.isFullscreen = isFullscreen + self.sameOrigin = sameOrigin self.appSettings = appSettings } } @@ -520,7 +523,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } #endif*/ - if !"".isEmpty { + if self.controller?.sameOrigin == true { self.webView?.bindTrustedOrigin(from: url) } else { self.webView?.setupEventProxySource() @@ -566,6 +569,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } if let parsedUrl = URL(string: result.url) { strongSelf.queryId = result.queryId + strongSelf.controller?.sameOrigin = result.flags.contains(.sameOrigin) strongSelf.load(url: parsedUrl) } }) @@ -586,6 +590,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified) + self.controller?.sameOrigin = result.flags.contains(.sameOrigin) self.load(url: parsedUrl) }) }) @@ -596,6 +601,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } strongSelf.queryId = result.queryId + strongSelf.controller?.sameOrigin = result.flags.contains(.sameOrigin) strongSelf.load(url: parsedUrl) if let keepAliveSignal = result.keepAliveSignal { @@ -3562,6 +3568,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let replyToMessageId: EngineMessage.Id? private let threadId: Int64? public var isFullscreen: Bool + private var sameOrigin: Bool private var pendingExternalUrl: String? private var presentationData: PresentationData @@ -3596,6 +3603,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.replyToMessageId = replyToMessageId self.threadId = threadId self.isFullscreen = params.isFullscreen + self.sameOrigin = params.sameOrigin self.updatedPresentationData = updatedPresentationData @@ -3708,9 +3716,9 @@ public final class WebAppController: ViewController, AttachmentContainable { id: "close", component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: self.controllerNode.hasBackButton ? "back" : "close", component: AnyComponent( BundleIconComponent( name: self.controllerNode.hasBackButton ? "Navigation/Back" : "Navigation/Close", @@ -3727,9 +3735,9 @@ public final class WebAppController: ViewController, AttachmentContainable { id: "more", component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "more", component: AnyComponent( LottieComponent( content: LottieComponent.AppBundleContent( @@ -4135,23 +4143,18 @@ public final class WebAppController: ViewController, AttachmentContainable { public func requestDismiss(completion: @escaping () -> Void) { if self.controllerNode.needDismissConfirmation { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: self.presentationData.strings.WebApp_CloseConfirmation), - ActionSheetButtonItem(title: self.presentationData.strings.WebApp_CloseAnyway, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - + let alertController = textAlertController( + context: self.context, + title: nil, + text: self.presentationData.strings.WebApp_CloseConfirmation, + actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.WebApp_CloseAnyway, action: { completion() }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.present(actionSheet, in: .window(.root)) + ] + ) + self.present(alertController, in: .window(.root)) } else { completion() } diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 5550078d1c..0f06104c94 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -222,8 +222,6 @@ private final class SheetContent: CombinedComponent { transition: .immediate ) context.add(button - .clipsToBounds(true) - .cornerRadius(10.0) .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index acebade7a2..de27125c74 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -70,7 +70,7 @@ private func securedEventProxySource(trustedOrigin: String) -> String { """ } -private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';" +private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([contenteditable=\"true\"]){-webkit-user-select:none;}';" + " var head = document.head || document.getElementsByTagName('head')[0];" + " var style = document.createElement('style'); style.type = 'text/css';" + " style.appendChild(document.createTextNode(css)); head.appendChild(style);"