From f7a65d8edd9c66c2fac91e60baff24ec8d119568 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Wed, 3 Jan 2024 13:00:33 +0000 Subject: [PATCH] Release 1.5.0 (210) --- Nicegram/NGAppCache/BUILD | 2 +- Nicegram/NGAppCache/Sources/AppCache.swift | 11 - .../NGOnboarding/Sources/OnboardingFlow.swift | 11 +- .../Sources/OnboardingViewController.swift | 6 +- Nicegram/NGRoundedVideos/BUILD | 15 + .../Sources/NGRoundedVideos.swift | 117 ++ Nicegram/NGStealthMode/BUILD | 18 + .../NGStealthMode/Sources/NGStealthMode.swift | 70 + Nicegram/NGStrings/BUILD | 3 +- Nicegram/NGStrings/Sources/Strings.swift | 166 +- .../SpeechToText/TgVoiceToTextProcessor.swift | 2 +- Nicegram/NGUI/BUILD | 1 + .../Sources/NicegramSettingsController.swift | 29 +- Nicegram/NGUI/Sources/PremiumController.swift | 21 +- Package.resolved | 2 +- Telegram/BUILD | 1 + Telegram/Telegram-iOS/Resources/Celebrate.tgs | Bin 0 -> 14103 bytes .../ar.lproj/NiceLocalizable.strings | 20 +- .../da.lproj/NiceLocalizable.strings | 10 - .../de.lproj/NiceLocalizable.strings | 18 +- .../Telegram-iOS/en.lproj/Localizable.strings | 229 ++- .../en.lproj/NiceLocalizable.strings | 10 +- .../es.lproj/NiceLocalizable.strings | 18 +- .../fr.lproj/NiceLocalizable.strings | 38 +- .../it.lproj/NiceLocalizable.strings | 24 +- .../km.lproj/NiceLocalizable.strings | 89 + .../ko.lproj/NiceLocalizable.strings | 24 +- .../pl.lproj/NiceLocalizable.strings | 22 +- .../pt.lproj/NiceLocalizable.strings | 26 +- .../ro.lproj/NiceLocalizable.strings | 1 - .../ru.lproj/NiceLocalizable.strings | 28 +- .../tr.lproj/NiceLocalizable.strings | 24 +- .../uk.lproj/NiceLocalizable.strings | 10 - .../vi.lproj/NiceLocalizable.strings | 1 - .../zh-hans.lproj/NiceLocalizable.strings | 24 +- .../zh-hant.lproj/NiceLocalizable.strings | 18 +- Tests/CallUITest/Sources/ViewController.swift | 43 +- .../Sources/AccountContext.swift | 107 +- .../Sources/ChatController.swift | 8 +- .../Sources/ChatListController.swift | 1 + .../ContactMultiselectionController.swift | 2 + .../Sources/AnimatedStickerFrameSource.swift | 4 +- .../Sources/AnimatedStickerNode.swift | 13 +- .../AppBundle/Sources/AppBundle/AppBundle.m | 24 +- .../AttachmentTextInputPanelNode.swift | 7 +- .../AvatarNode/Sources/AvatarNode.swift | 2 - .../Sources/BotCheckoutControllerNode.swift | 9 +- ...BotCheckoutNativeCardEntryController.swift | 2 +- ...heckoutNativeCardEntryControllerNode.swift | 6 +- .../Sources/ChatListController.swift | 52 +- .../Sources/ChatListSearchListPaneNode.swift | 27 +- .../Sources/ChatListShimmerNode.swift | 3 +- .../Sources/Node/ChatListItem.swift | 93 +- .../Sources/Node/ChatListItemStrings.swift | 6 + .../Sources/Node/ChatListNode.swift | 84 +- .../Sources/Node/ChatListNodeEntries.swift | 3 +- .../Sources/Node/ChatListNodeLocation.swift | 96 + .../Node/ChatListStorageInfoItem.swift | 46 +- .../ChatMessageInteractiveMediaBadge.swift | 4 +- .../ChatPresentationInterfaceState.swift | 140 +- ...ChatSendMessageActionSheetController.swift | 11 +- .../Source/Base/Transition.swift | 5 + .../Source/Components/RoundedRectangle.swift | 23 +- .../Sources/ComponentDisplayAdapters.swift | 4 +- .../ReactionListContextMenuContent.swift | 2 +- .../Sources/ViewControllerComponent.swift | 32 +- .../Sources/ContactListNode.swift | 78 +- .../Sources/ContactsControllerNode.swift | 2 +- .../Sources/ContactsPeerItem.swift | 85 +- .../ContextUI/Sources/ContextController.swift | 4 +- .../ContextControllerActionsStackNode.swift | 5 + ...tControllerExtractedPresentationNode.swift | 4 +- .../Sources/DebugController.swift | 38 +- .../Display/Source/ChildWindowHostView.swift | 6 +- .../ContainedViewLayoutTransition.swift | 50 + .../Display/Source/ContainerViewLayout.swift | 5 +- .../Source/ContextContentContainerNode.swift | 2 +- submodules/Display/Source/GenerateImage.swift | 70 +- submodules/Display/Source/ListView.swift | 4 +- .../Display/Source/ListViewItemNode.swift | 6 +- .../Display/Source/NativeWindowHostView.swift | 59 +- .../Display/Source/ViewController.swift | 4 +- submodules/Display/Source/WindowContent.swift | 28 +- submodules/DrawingUI/BUILD | 1 + .../Sources/DrawingEntitiesView.swift | 11 +- .../DrawingUI/Sources/DrawingScreen.swift | 62 +- .../Sources/DrawingStickerEntityView.swift | 384 +++- .../Sources/DrawingTextEntityView.swift | 14 +- .../DrawingUI/Sources/DrawingUtils.swift | 56 + .../Sources/ImageObjectSeparation.swift | 22 +- .../Sources/StickerPickerScreen.swift | 5 +- submodules/Emoji/Sources/EmojiUtils.swift | 23 - .../Sources/HashtagSearchController.swift | 2 + .../Sources/InAppPurchaseManager.swift | 18 +- .../Sources/ItemListPeerActionItem.swift | 45 +- .../Sources/ItemListPeerItem.swift | 2 +- .../Sources/ItemListControllerNode.swift | 2 +- .../ItemListUI/Sources/ItemListItem.swift | 4 + .../Sources/Items/ItemListActionItem.swift | 8 +- .../TGMediaPickerSendActionSheetController.h | 6 + .../TGModernConversationInputMicButton.h | 1 + .../Sources/NGLegacyComponents.h | 3 + .../Sources/NGLegacyComponents.m | 18 + .../TGMediaPickerSendActionSheetController.m | 59 +- .../TGModernConversationInputMicButton.m | 17 +- submodules/LegacyMediaPickerUI/BUILD | 6 +- .../Sources/LegacyMediaPickers.swift | 156 +- .../Sources/LegacyPaintStickersContext.swift | 4 + .../Sources/LegacyWallpaperPicker.swift | 75 + .../Sources/Nicegram/NGRoundedVideos.swift | 32 + .../Sources/ListSectionHeaderNode.swift | 1 + submodules/MediaPickerUI/BUILD | 6 +- .../Sources/LegacyMediaPickerGallery.swift | 64 +- .../Sources/MediaPickerScreen.swift | 115 +- .../Sources/MediaPickerSelectedListNode.swift | 4 +- .../FFMpegMediaVideoFrameDecoder.swift | 1 + .../Sources/SoftwareVideoSource.swift | 1 + .../UniversalSoftwareVideoSource.swift | 1 + submodules/MetalEngine/Package.swift | 30 + .../MetalEngine/Sources/MetalEngine.swift | 48 +- .../Sources/PeerInfoAvatarListNode.swift | 8 +- .../Postbox/Sources/CachedPeerData.swift | 2 +- .../Sources/MessageHistoryViewState.swift | 18 +- submodules/PremiumUI/BUILD | 1 + .../Sources/CreateGiveawayController.swift | 399 ++-- .../Sources/CreateGiveawayFooterItem.swift | 4 +- .../Sources/GiftAvatarComponent.swift | 89 +- .../Sources/GiveawayInfoController.swift | 112 +- .../Sources/LimitsPageComponent.swift | 22 +- .../Sources/PremiumBoostLevelsScreen.swift | 1657 +++++++++++++++++ .../Sources/PremiumBoostScreen.swift | 8 +- .../Sources/PremiumGiftCodeScreen.swift | 30 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 550 ++++-- .../Sources/PremiumIntroScreen.swift | 440 +++-- .../Sources/PremiumLimitScreen.swift | 10 +- .../Sources/ReplaceBoostScreen.swift | 36 +- .../Sources/RadialStatusNode.swift | 61 +- .../Reachability/Sources/Reachability.swift | 6 +- .../Sources/ReactionContextNode.swift | 2 + .../Source/Signal_Combine.swift | 6 + .../Sources/SelectablePeerNode.swift | 4 +- submodules/SettingsUI/BUILD | 7 + .../Search/SettingsSearchableItems.swift | 1 + .../TextSizeSelectionController.swift | 7 +- .../Sources/ThemePickerController.swift | 2 + .../Sources/ThemePickerGridItem.swift | 37 +- .../Themes/CustomWallpaperPicker.swift | 349 +--- .../Sources/Themes/EditThemeController.swift | 2 + .../ThemeAutoNightSettingsController.swift | 1 + .../Themes/ThemeGridControllerItem.swift | 133 -- .../Themes/ThemePreviewControllerNode.swift | 8 +- .../Themes/ThemeSettingsController.swift | 35 +- .../Sources/ShareControllerNode.swift | 2 +- .../Sources/ShareControllerPeerGridItem.swift | 7 +- .../Sources/SharePeersContainerNode.swift | 4 +- .../Sources/SolidRoundedButtonNode.swift | 45 +- .../Sources/MessageStatsController.swift | 66 +- .../Sources/StatsOverviewItem.swift | 13 +- submodules/StickerPackPreviewUI/BUILD | 6 +- .../Sources/StickerPackScreen.swift | 38 +- submodules/TelegramApi/Sources/Api0.swift | 44 +- submodules/TelegramApi/Sources/Api10.swift | 48 +- submodules/TelegramApi/Sources/Api11.swift | 18 +- submodules/TelegramApi/Sources/Api12.swift | 118 +- submodules/TelegramApi/Sources/Api13.swift | 82 +- submodules/TelegramApi/Sources/Api2.swift | 134 +- submodules/TelegramApi/Sources/Api21.swift | 364 ++-- submodules/TelegramApi/Sources/Api22.swift | 402 ++++ submodules/TelegramApi/Sources/Api23.swift | 18 +- submodules/TelegramApi/Sources/Api26.swift | 18 +- submodules/TelegramApi/Sources/Api29.swift | 14 +- submodules/TelegramApi/Sources/Api3.swift | 50 +- submodules/TelegramApi/Sources/Api30.swift | 246 +-- submodules/TelegramApi/Sources/Api31.swift | 172 ++ submodules/TelegramApi/Sources/Api32.swift | 113 +- .../Sources/ManagedAudioSession.swift | 8 +- .../Sources/CallController.swift | 44 +- .../Sources/CallControllerNode.swift | 24 +- .../Sources/CallControllerNodeV2.swift | 371 +++- .../Sources/CallKitIntegration.swift | 57 +- .../Components/MediaStreamComponent.swift | 9 + .../Components/ParticipantsComponent.swift | 10 +- .../Components/StreamSheetComponent.swift | 9 +- .../Sources/PresentationCall.swift | 40 +- .../Sources/VoiceChatParticipantItem.swift | 2 +- .../Account/AccountIntermediateState.swift | 2 +- .../Sources/Account/AccountManager.swift | 1 + .../Sources/ApiUtils/ApiGroupOrChannel.swift | 38 +- .../ReplyMarkupMessageAttribute.swift | 4 +- .../ApiUtils/StoreMessage_Telegram.swift | 37 +- .../ApiUtils/TelegramMediaAction.swift | 10 +- .../Sources/ApiUtils/TelegramMediaFile.swift | 2 + .../Sources/ApiUtils/Wallpaper.swift | 17 +- .../TelegramCore/Sources/ForumChannels.swift | 70 +- .../Sources/MacOS/MacInternalUpdater.swift | 4 +- .../Network/FetchedMediaResource.swift | 8 +- .../Sources/Network/Network.swift | 2 +- ...tworkFrameworkTcpConnectionInterface.swift | 2 +- .../PendingMessages/RequestEditMessage.swift | 4 +- .../State/AccountStateManagementUtils.swift | 66 +- .../Sources/State/AccountStateManager.swift | 10 +- .../Sources/State/AccountTaskManager.swift | 1 - .../Sources/State/AccountViewTracker.swift | 6 +- .../Sources/State/AppChangelog.swift | 35 +- .../Sources/State/ApplyUpdateMessage.swift | 4 +- .../Sources/State/CallSessionManager.swift | 16 +- .../Sources/State/FetchChatList.swift | 14 +- .../State/HistoryViewStateValidation.swift | 8 +- .../TelegramCore/Sources/State/Holes.swift | 8 +- .../Sources/State/ManagedChatListHoles.swift | 2 +- .../State/ManagedPeerColorUpdates.swift | 16 +- .../ManagedSecretChatOutgoingOperations.swift | 10 +- ...agedSynchronizePinnedChatsOperations.swift | 2 +- .../Sources/State/ResetState.swift | 2 +- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/UpdatesApiUtils.swift | 2 +- .../State/UserLimitsConfiguration.swift | 2 +- .../Sources/Statistics/StoryStatistics.swift | 54 +- .../TelegramCore/Sources/Suggestions.swift | 1 + .../SyncCore/SyncCore_CachedChannelData.swift | 81 +- ...SyncCore_ReplyMarkupMessageAttribute.swift | 7 +- .../SyncCore_StickerPackCollectionInfo.swift | 4 + .../SyncCore/SyncCore_TelegramChannel.swift | 81 +- .../SyncCore_TelegramMediaAction.swift | 63 +- .../SyncCore_TelegramMediaGiveaway.swift | 16 +- ...yncCore_TelegramMediaGiveawayResults.swift | 118 ++ .../SyncCore/SyncCore_TelegramWallpaper.swift | 99 +- .../TelegramEngine/Calls/GroupCalls.swift | 2 +- .../TelegramEngine/Data/PeersData.swift | 34 + .../TelegramEngine/Messages/AdMessages.swift | 8 +- .../Messages/EngineStoryViewListContext.swift | 711 +++++-- .../Messages/LoadMessagesIfNecessary.swift | 25 +- .../TelegramEngine/Messages/Media.swift | 7 + .../TelegramEngine/Messages/MediaArea.swift | 12 + .../Messages/ReplyThreadHistory.swift | 11 +- .../Messages/SearchMessages.swift | 61 +- .../TelegramEngine/Messages/SendAsPeers.swift | 2 +- .../Messages/SparseMessageList.swift | 2 +- .../TelegramEngine/Messages/Stories.swift | 53 +- .../Messages/StoryListContext.swift | 22 +- .../Messages/TelegramEngineMessages.swift | 13 +- .../TelegramEngine/Payments/AppStore.swift | 12 +- .../Payments/BotPaymentForm.swift | 49 +- .../TelegramEngine/Payments/GiftCodes.swift | 53 +- .../Payments/TelegramEnginePayments.swift | 6 +- .../TelegramEngine/Peers/AddPeerMember.swift | 25 +- .../TelegramEngine/Peers/AddressNames.swift | 2 +- .../Peers/ChannelAdminEventLogs.swift | 84 +- .../Peers/ChannelRecommendation.swift | 2 +- .../Peers/ChatListFiltering.swift | 2 +- .../TelegramEngine/Peers/Communities.swift | 6 +- .../Peers/InactiveChannels.swift | 2 +- .../Sources/TelegramEngine/Peers/Peer.swift | 4 + .../Peers/RequestUserPhotos.swift | 4 +- .../TelegramEngine/Peers/SearchPeers.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 19 +- .../Peers/UpdateCachedPeerData.swift | 7 +- .../TelegramEngine/Peers/UpdatePeerInfo.swift | 213 ++- .../UpdatedAccountPrivacySettings.swift | 4 +- .../TelegramEngine/Stickers/StickerPack.swift | 3 + .../TelegramEngine/Themes/ChatThemes.swift | 2 + .../TelegramCore/Sources/UpdatePeers.swift | 2 +- .../Sources/Utils/PeerUtils.swift | 11 + .../TelegramCore/Sources/WebpagePreview.swift | 118 +- .../ChatControllerBackgroundNode.swift | 4 +- .../Sources/PresentationData.swift | 30 + .../PresentationThemeEssentialGraphics.swift | 8 +- .../PresentationResourcesSettings.swift | 3 +- .../Sources/WallpaperUtils.swift | 11 + .../Sources/ServiceMessageStrings.swift | 64 +- submodules/TelegramUI/BUILD | 4 +- .../Components/Calls/CallScreen/BUILD | 1 + .../EmojiTooltipLock.imageset/Contents.json | 12 + .../locksettings (1).pdf | Bin 0 -> 2505 bytes .../CallScreen/Metal/CallScreenShaders.metal | 18 +- .../Sources/Components/ButtonGroupView.swift | 61 +- .../Sources/Components/CloseButtonView.swift | 185 ++ .../Components/ContentOverlayButton.swift | 21 +- .../Components/EmojiExpandedInfoView.swift | 20 +- .../Sources/Components/EmojiTooltipView.swift | 21 +- .../Sources/Components/NoticeView.swift | 24 +- .../PrivateCallPictureInPictureView.swift | 248 +++ .../Components/PrivateCallVideoLayer.swift | 41 +- .../Sources/Components/RatingView.swift | 73 + .../Components/RoundedCornersView.swift | 46 +- .../Sources/Components/StatusView.swift | 15 +- .../Components/VideoContainerView.swift | 137 +- .../CallScreen/Sources/Media/VideoInput.swift | 72 +- .../Sources/PrivateCallScreen.swift | 347 +++- .../Sources/ShutterBlobView.swift | 2 + .../Sources/ChatButtonKeyboardInputNode.swift | 4 +- .../ChatChannelSubscriberInputPanelNode/BUILD | 1 + .../ChatChannelSubscriberInputPanelNode.swift | 35 +- .../ChatMessageAttachedContentNode.swift | 97 +- .../Sources/ChatMessageBubbleItemNode.swift | 39 +- .../StringForMessageTimestampStatus.swift | 74 +- .../Sources/ChatMessageForwardInfoNode.swift | 2 + .../ChatMessageGiftBubbleContentNode.swift | 52 +- ...ChatMessageGiveawayBubbleContentNode.swift | 310 ++- .../ChatMessageInteractiveFileNode.swift | 32 +- ...atMessageInteractiveInstantVideoNode.swift | 67 +- .../ChatMessageInteractiveMediaNode.swift | 66 +- .../Sources/ChatMessageDateHeader.swift | 21 +- .../BUILD | 1 + ...essageJoinedChannelBubbleContentNode.swift | 3 +- .../ChatMessageMapBubbleContentNode.swift | 10 +- .../ChatMessageMediaBubbleContentNode.swift | 8 +- .../ChatMessagePollBubbleContentNode/BUILD | 1 + .../ChatMessagePollBubbleContentNode.swift | 212 +-- .../Sources/ChatMessageReplyInfoNode.swift | 7 +- .../Sources/ChatMessageStickerItemNode.swift | 8 +- .../ChatMessageTextBubbleContentNode.swift | 53 +- ...hatMessageWallpaperBubbleContentNode.swift | 35 +- .../Chat/ChatRecentActionsController/BUILD | 1 + .../ChatRecentActionsControllerNode.swift | 2 +- .../ChatRecentActionsHistoryTransition.swift | 338 +++- .../Sources/EditableTokenListNode.swift | 37 +- .../Components/Chat/MergedAvatarsNode/BUILD | 25 + .../Sources/MergedAvatarsNode.swift | 217 +++ .../MessageInlineBlockBackgroundView.swift | 18 +- .../Sources/ChatControllerInteraction.swift | 4 +- .../Sources/ChatEntityKeyboardInputNode.swift | 2 +- .../ChatTextInputMediaRecordingButton.swift | 37 + .../Sources/LockView.swift | 1 + .../ChatTitleView/Sources/ChatTitleView.swift | 172 +- .../Sources/ContextMenuNode.swift | 18 +- .../DustEffect/Metal/DustEffectShaders.metal | 6 +- .../DustEffect/Sources/DustEffectLayer.swift | 15 +- .../Components/DynamicCornerRadiusView/BUILD | 18 + .../Sources/DynamicCornerRadiusView.swift | 90 + .../EmojiStatusSelectionComponent.swift | 102 +- .../Sources/EmojiTextAttachmentView.swift | 85 +- .../Components/EntityKeyboard/BUILD | 6 +- .../Sources/EmojiPagerContentComponent.swift | 255 ++- .../Sources/LegacyMessageInputPanel.swift | 2 +- .../Components/ListActionItemComponent/BUILD | 21 + .../Sources/ListActionItemComponent.swift | 187 ++ .../Components/ListItemComponentAdaptor/BUILD | 21 + .../Sources/ListItemComponentAdaptor.swift | 131 ++ .../Components/ListSectionComponent/BUILD | 21 + .../Sources/ListSectionComponent.swift | 288 +++ .../TelegramUI/Components/MediaEditor/BUILD | 1 + .../Drawing/CodableDrawingEntity.swift | 25 +- .../Sources/Drawing/DrawingMediaEntity.swift | 163 +- .../Drawing/DrawingStickerEntity.swift | 57 +- .../Sources/DrawingMessageRenderer.swift | 297 +++ .../MediaEditor/Sources/MediaEditor.swift | 72 +- .../Sources/MediaEditorComposerEntity.swift | 14 + .../Sources/MediaEditorDraft.swift | 9 +- .../Sources/MediaEditorUtils.swift | 112 ++ .../Sources/MediaEditorValues.swift | 57 +- .../Sources/MediaEditorDrafts.swift | 127 +- .../Sources/MediaEditorScreen.swift | 497 +++-- .../Sources/MediaToolsScreen.swift | 14 +- .../Sources/ContextResultPanelComponent.swift | 4 +- .../NotificationExceptionsScreen.swift | 2 +- .../PeerAllowedReactionsScreen/BUILD | 1 + .../Sources/PeerAllowedReactionsScreen.swift | 79 +- .../PeerInfo/PeerInfoChatListPaneNode/BUILD | 29 + .../Sources/PeerInfoChatListPaneNode.swift | 206 ++ .../PeerInfo/PeerInfoPaneNode/BUILD | 23 + .../Sources/PeerInfoPaneNode.swift | 57 + .../Components/PeerInfo/PeerInfoScreen/BUILD | 5 + .../PeerInfoScreenDisclosureItem.swift | 28 +- .../Sources/Panes/PeerInfoGifPaneNode.swift | 1 + .../PeerInfoGroupsInCommonPaneNode.swift | 1 + .../Sources/Panes/PeerInfoListPaneNode.swift | 1 + .../Sources/Panes/PeerInfoMembersPane.swift | 1 + .../PeerInfoRecommendedChannelsPane.swift | 1 + .../PeerInfoScreen/Sources/PeerInfoData.swift | 34 +- .../PeerInfoHeaderNavigationButton.swift | 76 +- ...oHeaderNavigationButtonContainerNode.swift | 18 +- .../Sources/PeerInfoHeaderNode.swift | 165 +- .../Sources/PeerInfoPaneContainerNode.swift | 6 + .../Sources/PeerInfoScreen.swift | 481 ++--- .../PeerInfoVisualMediaPaneNode/BUILD | 1 + .../Sources/PeerInfoStoryPaneNode.swift | 1 + .../Sources/PeerInfoVisualMediaPaneNode.swift | 55 +- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../PremiumLockButtonSubtitleComponent/BUILD | 22 + .../PremiumLockButtonSubtitleComponent.swift | 87 + .../Sources/FetchVideoMediaResource.swift | 34 +- .../SavedMessages/SavedMessagesScreen/BUILD | 26 + .../Sources/SavedMessagesScreen.swift | 86 + .../Settings/BoostLevelIconComponent/BUILD | 20 + .../Sources/BoostLevelIconComponent.swift | 105 ++ .../Settings/GenerateThemeName/BUILD | 18 + .../Sources}/GenerateGradientColors.swift | 2 +- .../Sources}/GenerateThemeName.swift | 2 +- .../Settings/PeerNameColorScreen/BUILD | 19 + .../Sources/ChannelAppearanceScreen.swift | 1554 ++++++++++++++++ .../PeerNameColorChatPreviewItem.swift | 96 +- .../Sources/PeerNameColorItem.swift | 68 +- .../PeerNameColorProfilePreviewItem.swift | 223 ++- .../Sources/PeerNameColorScreen.swift | 9 +- .../Settings/SettingsThemeWallpaperNode/BUILD | 30 + .../Sources}/SettingsThemeWallpaperNode.swift | 110 +- .../Settings/ThemeAccentColorScreen/BUILD | 40 + .../Sources}/ThemeAccentColorController.swift | 22 +- .../ThemeAccentColorControllerNode.swift | 20 +- .../ThemeColorSegmentedTitleView.swift | 0 .../Settings/ThemeCarouselItem/BUILD | 37 + .../Sources/ThemeCarouselItem.swift | 252 ++- .../Settings/ThemeSettingsThemeItem/BUILD | 33 + .../Sources}/ThemeSettingsThemeItem.swift | 129 +- .../Settings/WallpaperGalleryScreen/BUILD | 39 + .../Sources/BlurredImageNode.swift | 16 +- .../Sources}/WallpaperColorPanelNode.swift | 51 +- .../Sources}/WallpaperColorPickerNode.swift | 26 +- .../Sources}/WallpaperCropNode.swift | 0 .../Sources}/WallpaperGalleryController.swift | 13 +- .../Sources}/WallpaperGalleryItem.swift | 84 +- .../WallpaperGalleryToolbarNode.swift | 122 +- .../Sources}/WallpaperOptionButtonNode.swift | 24 +- .../Sources}/WallpaperPatternPanelNode.swift | 25 +- .../Settings/WallpaperGridScreen/BUILD | 44 + .../Sources}/ThemeColorsGridController.swift | 2 + .../ThemeColorsGridControllerItem.swift | 1 + .../ThemeColorsGridControllerNode.swift | 3 +- .../Sources}/ThemeGridController.swift | 185 +- .../Sources/ThemeGridControllerItem.swift | 229 +++ .../Sources}/ThemeGridControllerNode.swift | 442 +++-- .../Sources}/ThemeGridSearchColorsItem.swift | 0 .../Sources}/ThemeGridSearchContentNode.swift | 0 .../Sources}/ThemeGridSearchItem.swift | 0 .../ThemeGridSelectionPanelNode.swift | 0 .../WallpaperSearchRecentQueries.swift | 0 .../Sources/WallpaperUtils.swift | 377 ++++ .../Sources/ShareWithPeersScreen.swift | 192 +- .../Sources/StorageUsageScreen.swift | 4 +- .../Sources/ForwardInfoPanelComponent.swift | 1 + .../Stories/PeerListItemComponent/BUILD | 1 + .../Sources/PeerListItemComponent.swift | 136 +- .../Sources/StoryChatContent.swift | 786 +++++++- .../StoryContentCaptionComponent.swift | 12 +- .../Sources/StoryItemContentComponent.swift | 40 +- .../StoryItemSetContainerComponent.swift | 216 ++- ...StoryItemSetContainerViewSendMessage.swift | 74 +- .../StoryItemSetViewListComponent.swift | 261 ++- .../Sources/StoryFooterPanelComponent.swift | 809 ++++---- .../Sources/EditableTokenListNode.swift | 1 + .../Sources/WallpaperPreviewMedia.swift | 3 + .../Premium/BoostPerk/Contents.json | 9 + .../CoverColor.imageset/Contents.json | 12 + .../CoverColor.imageset/covercolor_30.pdf | Bin 0 -> 10860 bytes .../CoverLogo.imageset/Contents.json | 12 + .../CoverLogo.imageset/coverlogo_30.pdf | Bin 0 -> 8922 bytes .../CustomWallpaper.imageset/Contents.json | 12 + .../backgroundphoto_30.pdf | Bin 0 -> 9789 bytes .../EmojiStatus.imageset/Contents.json | 12 + .../EmojiStatus.imageset/status_30.pdf | Bin 0 -> 3542 bytes .../LinkColor.imageset/Contents.json | 12 + .../LinkColor.imageset/linkcolor_30.pdf | Bin 0 -> 7498 bytes .../BoostPerk/LinkLogo.imageset/Contents.json | 12 + .../LinkLogo.imageset/linkicon_30.pdf | Bin 0 -> 7604 bytes .../NameColor.imageset/Contents.json | 12 + .../NameColor.imageset/namecolor_30.pdf | Bin 0 -> 10475 bytes .../BoostPerk/Reaction.imageset/Contents.json | 12 + .../Reaction.imageset/reaction_30.pdf | Bin 0 -> 2213 bytes .../BoostPerk/Story.imageset/Contents.json | 12 + .../Story.imageset/storiescircle_30.pdf | Bin 0 -> 4791 bytes .../Wallpaper.imageset/Contents.json | 12 + .../Wallpaper.imageset/background_30.pdf | Bin 0 -> 8648 bytes .../Settings/Menu/Gift.imageset/Contents.json | 12 + .../Settings/Menu/Gift.imageset/gifting.pdf | Bin 0 -> 8960 bytes .../HeaderForward.imageset/Contents.json | 12 + .../Stories/HeaderForward.imageset/arrow.pdf | Bin 0 -> 9372 bytes .../TelegramUI/Resources/Animations/Lock.json | 2 +- .../Resources/Animations/LockWait.json | 2 +- .../TelegramUI/Sources/AppDelegate.swift | 62 +- .../Sources/ApplicationContext.swift | 4 +- .../Sources/AttachmentFileController.swift | 3 + .../ChatControllerNavigateToMessage.swift | 7 +- .../Chat/ChatMessageActionOptions.swift | 2 +- ...ChatMessageDisplaySendMessageOptions.swift | 14 +- ...UpdateChatPresentationInterfaceState.swift | 6 +- .../TelegramUI/Sources/ChatController.swift | 816 +++++--- .../Sources/ChatControllerNode.swift | 10 +- .../Sources/ChatHistoryListNode.swift | 119 +- .../ChatInterfaceStateContextMenus.swift | 20 +- .../ChatInterfaceStateContextQueries.swift | 4 +- .../ChatInterfaceStateInputPanels.swift | 11 + .../ChatInterfaceStateNavigationButtons.swift | 6 +- .../ChatReportPeerTitlePanelNode.swift | 5 + .../ChatSearchResultsContollerNode.swift | 2 + .../Sources/ComposeControllerNode.swift | 2 +- .../ContactMultiselectionController.swift | 63 +- .../ContactMultiselectionControllerNode.swift | 136 +- .../ContactSelectionControllerNode.swift | 2 +- .../Sources/NavigateToChatController.swift | 9 + .../Sources/Nicegram/NGDeeplinkHandler.swift | 13 +- .../TelegramUI/Sources/OpenChatMessage.swift | 1 + .../TelegramUI/Sources/OpenResolvedUrl.swift | 132 +- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/SharedAccountContext.swift | 209 ++- .../TelegramUI/Sources/TextLinkHandling.swift | 2 +- .../Sources/ExperimentalUISettings.swift | 24 +- .../Sources/ChatTextInputAttributes.swift | 22 +- submodules/TgVoipWebrtc/tgcalls | 2 +- .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 16 +- .../UrlHandling/Sources/UrlHandling.swift | 14 +- submodules/Utils/LokiRng/Package.swift | 1 + submodules/Utils/ShelfPack/Package.swift | 29 + .../Sources/WallpaperResources.swift | 28 +- .../WebUI/Sources/WebAppController.swift | 13 +- submodules/WebUI/Sources/WebAppWebView.swift | 17 +- submodules/rlottie/LottieInstance.mm | 5 + swift_deps.bzl | 2 +- swift_deps_index.json | 29 +- versions.json | 2 +- 511 files changed, 23097 insertions(+), 6534 deletions(-) create mode 100644 Nicegram/NGRoundedVideos/BUILD create mode 100644 Nicegram/NGRoundedVideos/Sources/NGRoundedVideos.swift create mode 100644 Nicegram/NGStealthMode/BUILD create mode 100644 Nicegram/NGStealthMode/Sources/NGStealthMode.swift create mode 100644 Telegram/Telegram-iOS/Resources/Celebrate.tgs create mode 100644 submodules/LegacyComponents/Sources/NGLegacyComponents.h create mode 100644 submodules/LegacyComponents/Sources/NGLegacyComponents.m create mode 100644 submodules/LegacyMediaPickerUI/Sources/Nicegram/NGRoundedVideos.swift create mode 100644 submodules/MetalEngine/Package.swift create mode 100644 submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift delete mode 100644 submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift create mode 100644 submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveawayResults.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/locksettings (1).pdf create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift create mode 100644 submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift create mode 100644 submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift create mode 100644 submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD create mode 100644 submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift create mode 100644 submodules/TelegramUI/Components/ListActionItemComponent/BUILD create mode 100644 submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift create mode 100644 submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD create mode 100644 submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift create mode 100644 submodules/TelegramUI/Components/ListSectionComponent/BUILD create mode 100644 submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/BUILD create mode 100644 submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift create mode 100644 submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD create mode 100644 submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift create mode 100644 submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD create mode 100644 submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift create mode 100644 submodules/TelegramUI/Components/Settings/GenerateThemeName/BUILD rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/GenerateThemeName/Sources}/GenerateGradientColors.swift (99%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/GenerateThemeName/Sources}/GenerateThemeName.swift (98%) create mode 100644 submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources}/SettingsThemeWallpaperNode.swift (73%) create mode 100644 submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources}/ThemeAccentColorController.swift (98%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources}/ThemeAccentColorControllerNode.swift (98%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources}/ThemeColorSegmentedTitleView.swift (100%) create mode 100644 submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD rename submodules/{SettingsUI => TelegramUI/Components/Settings/ThemeCarouselItem}/Sources/ThemeCarouselItem.swift (74%) create mode 100644 submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources}/ThemeSettingsThemeItem.swift (85%) create mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD rename submodules/{SettingsUI => TelegramUI/Components/Settings/WallpaperGalleryScreen}/Sources/BlurredImageNode.swift (91%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperColorPanelNode.swift (94%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperColorPickerNode.swift (97%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperCropNode.swift (100%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperGalleryController.swift (99%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperGalleryItem.swift (94%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperGalleryToolbarNode.swift (79%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperOptionButtonNode.swift (97%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources}/WallpaperPatternPanelNode.swift (96%) create mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/BUILD rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeColorsGridController.swift (99%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeColorsGridControllerItem.swift (98%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeColorsGridControllerNode.swift (99%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridController.swift (72%) create mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridControllerNode.swift (67%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridSearchColorsItem.swift (100%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridSearchContentNode.swift (100%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridSearchItem.swift (100%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/ThemeGridSelectionPanelNode.swift (100%) rename submodules/{SettingsUI/Sources/Themes => TelegramUI/Components/Settings/WallpaperGridScreen/Sources}/WallpaperSearchRecentQueries.swift (100%) create mode 100644 submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/covercolor_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/coverlogo_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/backgroundphoto_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/status_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/linkcolor_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/linkicon_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/namecolor_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Reaction.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Reaction.imageset/reaction_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/storiescircle_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/background_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/gifting.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf create mode 100644 submodules/Utils/ShelfPack/Package.swift diff --git a/Nicegram/NGAppCache/BUILD b/Nicegram/NGAppCache/BUILD index 774de4cc1ab..99e2cc5dc4d 100644 --- a/Nicegram/NGAppCache/BUILD +++ b/Nicegram/NGAppCache/BUILD @@ -7,7 +7,7 @@ swift_library( "Sources/**/*.swift", ]), deps = [ - "@swiftpkg_nicegram_assistant_ios//:Sources_NGCore" + ], visibility = [ "//visibility:public", diff --git a/Nicegram/NGAppCache/Sources/AppCache.swift b/Nicegram/NGAppCache/Sources/AppCache.swift index 6ea85315c6d..a9d7475091d 100644 --- a/Nicegram/NGAppCache/Sources/AppCache.swift +++ b/Nicegram/NGAppCache/Sources/AppCache.swift @@ -1,5 +1,4 @@ import Foundation -import NGCore public final class AppCache { // Old key 'appLaunchCount' @@ -32,16 +31,6 @@ public final class AppCache { _wasLauchedBefore = newValue } } - - public static var mobileIdentifier: String { - if let identifier = KeychainWrapper.standard.string(forKey: "ng_mobileIdentifier", withAccessibility: .afterFirstUnlock), - !identifier.isEmpty { - return identifier - } - let newIdentifier = UUID().uuidString - KeychainWrapper.standard.set(newIdentifier, forKey: "ng_mobileIdentifier", withAccessibility: .afterFirstUnlock) - return newIdentifier - } private init() {} } diff --git a/Nicegram/NGOnboarding/Sources/OnboardingFlow.swift b/Nicegram/NGOnboarding/Sources/OnboardingFlow.swift index 98bf9571531..19155853786 100644 --- a/Nicegram/NGOnboarding/Sources/OnboardingFlow.swift +++ b/Nicegram/NGOnboarding/Sources/OnboardingFlow.swift @@ -5,10 +5,9 @@ import NGAiChat import NGData import NGStrings -public func onboardingController(languageCode: String, onComplete: @escaping () -> Void) -> UIViewController { +public func onboardingController(onComplete: @escaping () -> Void) -> UIViewController { let controller = OnboardingViewController( - items: onboardingPages(languageCode: languageCode), - languageCode: languageCode, + items: onboardingPages(), onComplete: { if isPremium() { onComplete() @@ -23,7 +22,7 @@ public func onboardingController(languageCode: String, onComplete: @escaping () return controller } -private func onboardingPages(languageCode: String) -> [OnboardingPageViewModel] { +private func onboardingPages() -> [OnboardingPageViewModel] { let aiPageIndex = 6 var pages = Array(1...6) @@ -41,8 +40,8 @@ private func onboardingPages(languageCode: String) -> [OnboardingPageViewModel] return pages.map { index in OnboardingPageViewModel( - title: l("NicegramOnboarding.\(index).Title", languageCode), - description: l("NicegramOnboarding.\(index).Desc", languageCode), + title: l("NicegramOnboarding.\(index).Title"), + description: l("NicegramOnboarding.\(index).Desc"), videoURL: Bundle.main.url(forResource: "Nicegram_Onboarding-DS_v\(index)", withExtension: "mp4")! ) } diff --git a/Nicegram/NGOnboarding/Sources/OnboardingViewController.swift b/Nicegram/NGOnboarding/Sources/OnboardingViewController.swift index b59735365f5..d555424d73c 100644 --- a/Nicegram/NGOnboarding/Sources/OnboardingViewController.swift +++ b/Nicegram/NGOnboarding/Sources/OnboardingViewController.swift @@ -28,13 +28,11 @@ class OnboardingViewController: UIViewController { // MARK: - Logic private let items: [OnboardingPageViewModel] - private let languageCode: String // MARK: - Lifecycle - init(items: [OnboardingPageViewModel], languageCode: String, onComplete: @escaping () -> Void) { + init(items: [OnboardingPageViewModel], onComplete: @escaping () -> Void) { self.items = items - self.languageCode = languageCode self.onComplete = onComplete super.init(nibName: nil, bundle: nil) @@ -55,7 +53,7 @@ class OnboardingViewController: UIViewController { scrollView.delegate = self display(items: self.items) - display(buttonTitle: l("NicegramOnboarding.Continue", languageCode)) + display(buttonTitle: l("NicegramOnboarding.Continue")) nextButton.touchUpInside = { [weak self] in self?.goToNextPage() diff --git a/Nicegram/NGRoundedVideos/BUILD b/Nicegram/NGRoundedVideos/BUILD new file mode 100644 index 00000000000..a7215adc192 --- /dev/null +++ b/Nicegram/NGRoundedVideos/BUILD @@ -0,0 +1,15 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NGRoundedVideos", + module_name = "NGRoundedVideos", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//Nicegram/NGAppCache:NGAppCache", + "//Nicegram/NGStrings:NGStrings", + ], + visibility = ["//visibility:public"], + +) diff --git a/Nicegram/NGRoundedVideos/Sources/NGRoundedVideos.swift b/Nicegram/NGRoundedVideos/Sources/NGRoundedVideos.swift new file mode 100644 index 00000000000..3bd8ef89daf --- /dev/null +++ b/Nicegram/NGRoundedVideos/Sources/NGRoundedVideos.swift @@ -0,0 +1,117 @@ +import CoreMedia +import NGAppCache +import NGStrings +import UIKit + +public struct NGRoundedVideos { + public struct Constants { + public static let trimSeconds = 60.0 + public static let videoSize = CGSize( + width: 384, + height: 384 + ) + } +} + +public extension NGRoundedVideos { + // Workaround to reduce changes in telegram code + static var sendAsRoundedVideo = false + + static func calcCropRectAndScale( + originalCropRect: CGRect + ) -> (CGRect, CGFloat) { + let targetSize = NGRoundedVideos.Constants.videoSize + + let originalAspectRatio = originalCropRect.width / originalCropRect.height + let targetAspectRatio = targetSize.width / targetSize.height + + let trimmingSize: CGSize + if originalAspectRatio < targetAspectRatio { + trimmingSize = CGSize( + width: originalCropRect.width, + height: originalCropRect.width / targetAspectRatio + ) + } else { + trimmingSize = CGSize( + width: originalCropRect.height * targetAspectRatio, + height: originalCropRect.height + ) + } + + let extraWidth = originalCropRect.width - trimmingSize.width + let extraHeight = originalCropRect.height - trimmingSize.height + + let cropRect = CGRect( + origin: originalCropRect.origin.applying( + CGAffineTransform( + translationX: extraWidth / 2, + y: extraHeight / 2 + ) + ), + size: trimmingSize + ) + let cropScale = targetSize.width / cropRect.width + + return (cropRect, cropScale) + } + + static func trim(range: CMTimeRange) -> [CMTimeRange] { + let length = CMTime( + seconds: Constants.trimSeconds, + preferredTimescale: 60 + ) + + var chunks: [CMTimeRange] = [] + var from = range.start + while from < range.end { + chunks.append( + CMTimeRange( + start: from, + duration: length + ).intersection(range) + ) + from = from + length + } + + return chunks + } +} + +public extension NGRoundedVideos { + @UserDefaultsBacked( + key: "sawRoundedVideoMoreButtonTooltip", + defaultValue: false + ) + static var sawMoreButtonTooltip + + @UserDefaultsBacked( + key: "sawRoundedVideoSendButtonTooltip", + defaultValue: false + ) + static var sawSendButtonTooltip +} + +public extension NGRoundedVideos { + struct Resources {} +} +public extension NGRoundedVideos.Resources { + static func buttonTitle() -> String { + l("RoundedVideos.ButtonTitle") + } + + static func buttonIcon() -> UIImage? { + if #available(iOS 13.0, *) { + UIImage(systemName: "video.circle") + } else { + nil + } + } + + static func moreButtonTooltip() -> String { + l("RoundedVideos.MoreButtonTooltip") + } + + static func sendButtonTooltip() -> String { + l("RoundedVideos.SendButtonTooltip") + } +} diff --git a/Nicegram/NGStealthMode/BUILD b/Nicegram/NGStealthMode/BUILD new file mode 100644 index 00000000000..27a312c5aeb --- /dev/null +++ b/Nicegram/NGStealthMode/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NGStealthMode", + module_name = "NGStealthMode", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//Nicegram/NGStrings:NGStrings", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPremium", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Nicegram/NGStealthMode/Sources/NGStealthMode.swift b/Nicegram/NGStealthMode/Sources/NGStealthMode.swift new file mode 100644 index 00000000000..da3c5791f21 --- /dev/null +++ b/Nicegram/NGStealthMode/Sources/NGStealthMode.swift @@ -0,0 +1,70 @@ +import AccountContext +import FeatPremium +import NGCore +import NGStrings +import TelegramUIPreferences + +public struct NGStealthMode { + private static var stealthModeSubscription: Any? +} + +public extension NGStealthMode { + static func initialize( + sharedContext: SharedAccountContext + ) { + guard #available(iOS 13.0, *) else { + return + } + + let getPremiumStatusUseCase = PremiumContainer.shared.getPremiumStatusUseCase() + + stealthModeSubscription = $stealthModeEnabled + .combineLatest(getPremiumStatusUseCase.hasPremiumOnDevicePublisher()) + .map { stealthModeEnabled, hasPremium in + stealthModeEnabled && hasPremium + } + .sink { [weak sharedContext] useStealthMode in + guard let sharedContext else { + return + } + + let _ = updateExperimentalUISettingsInteractively(accountManager: sharedContext.accountManager, { settings in + var settings = settings + settings.skipReadHistory = useStealthMode + return settings + }).start() + } + } + + static func isStealthModeEnabled() -> Bool { + if #available(iOS 13.0, *) { + stealthModeEnabled + } else { + false + } + } + + static func setStealthModeEnabled(_ enabled: Bool) { + if #available(iOS 13.0, *) { + stealthModeEnabled = enabled + } + } +} + +public extension NGStealthMode { + struct Resources {} +} +public extension NGStealthMode.Resources { + static func toggleTitle() -> String { + l("StealthMode.Toggle") + } +} + +@available(iOS 13.0, *) +private extension NGStealthMode { + @UserDefaultsValue( + key: "stealthModeEnabled", + defaultValue: false + ) + static var stealthModeEnabled: Bool +} diff --git a/Nicegram/NGStrings/BUILD b/Nicegram/NGStrings/BUILD index d29b4382ed6..a9ac4002b8b 100644 --- a/Nicegram/NGStrings/BUILD +++ b/Nicegram/NGStrings/BUILD @@ -7,8 +7,7 @@ swift_library( "Sources/**/*.swift", ]), deps = [ - "//Nicegram/NGLogging:NGLogging", - "//submodules/AppBundle:AppBundle" + "@swiftpkg_nicegram_assistant_ios//:Sources_NGLocalization" ], visibility = [ "//visibility:public", diff --git a/Nicegram/NGStrings/Sources/Strings.swift b/Nicegram/NGStrings/Sources/Strings.swift index bcee663f70a..a798cd3087a 100644 --- a/Nicegram/NGStrings/Sources/Strings.swift +++ b/Nicegram/NGStrings/Sources/Strings.swift @@ -1,154 +1,36 @@ -// -// Strings.swift -// NGStrings -// -// Created by Sergey Akentev on 10/07/2019. -// Copyright © 2019 Nicegram. All rights reserved. -// - import Foundation -import AppBundle -import NGLogging - -fileprivate let LOGTAG = extractNameFromPath(#file) - -private func gd(locale: String) -> [String : String] { - return NSDictionary(contentsOf: URL(fileURLWithPath: getAppBundle().path(forResource: "NiceLocalizable", ofType: "strings", inDirectory: nil, forLocalization: locale)!)) as! [String : String] -} +import NGLocalization -let niceLocales: [String : [String : String]] = [ - "en" : gd(locale: "en"), - "ru": gd(locale: "ru"), - "ar": gd(locale: "ar"), - "de": gd(locale: "de"), - "it": gd(locale: "it"), - "es": gd(locale: "es"), - "fr": gd(locale: "fr"), - "pt": gd(locale: "pt"), +public func l(_ key: String, _ locale: String? = nil) -> String { + let code = locale ?? ng_getTgLangCode() + let table = "NiceLocalizable" - // Chinese - // Simplified - "zh-hans": gd(locale: "zh-hans"), - // Traditional - "zh-hant": gd(locale: "zh-hant"), + let bundle = localizationBundle(code: code) - "fa": gd(locale: "fa"), - "pl": gd(locale: "pl"), - "sk": gd(locale: "sk"), - "tr": gd(locale: "tr"), - "ro": gd(locale: "ro"), - "ko": gd(locale: "ko"), - "ku": gd(locale: "ku"), - "hi": gd(locale: "hi"), - "id": gd(locale: "id"), - "uk": gd(locale: "uk"), - "be": [:], -] - -public func getLangFallback(_ lang: String) -> String { - switch (lang) { - case "zh-hant": - return "zh-hans" - case "uk", "be": - return "ru" - case "ckb": - return "ku" - case "sdh": // Need investigate - return "ku" - default: - return "en" - } -} - -func getFallbackKey(_ key: String) -> String { - switch (key) { - case "NicegramSettings.Tabs": - return "NiceFeatures.Tabs.Header" - case "NicegramSettings.Tabs.showContactsTab": - return "NiceFeatures.Tabs.ShowContacts" - case "NicegramSettings.Tabs.showTabNames": - return "NiceFeatures.Tabs.ShowNames" - case "NicegramSettings.Folders": - return "NiceFeatures.Folders.Header" - case "NicegramSettings.Folders.foldersAtBottom": - return "NiceFeatures.Folders.TgFolders" - case "NicegramSettings.Folders.foldersAtBottomNotice": - return "NiceFeatures.Folders.TgFolders.Notice" - case "NicegramSettings.RoundVideos": - return "NiceFeatures.RoundVideos.Header" - case "NicegramSettings.RoundVideos.startWithRearCam": - return "NiceFeatures.RoundVideos.UseRearCamera" - case "NicegramSettings.Other.hidePhoneInSettings": - return "NiceFeatures.HideNumber" - - default: - return key - } -} - -public func l(_ key: String, _ locale: String = "en") -> String { - var lang = locale - let key = getFallbackKey(key) - let rawSuffix = "-raw" - if lang.hasSuffix(rawSuffix) { - lang = String(lang.dropLast(rawSuffix.count)) - } + let enBundle = localizationBundle(code: "en") + let enString = enBundle?.localizedString( + forKey: key, + value: key, + table: table + ) ?? key - if lang == "pt-br" { - lang = "pt" - } - - if !niceLocales.keys.contains(lang) { - lang = "en" - } - - var result = "[MISSING STRING. PLEASE UPDATE APP]" - - if let res = niceWebLocales[lang]?[key], !res.isEmpty { - result = res - } else if let res = niceLocales[lang]?[key], !res.isEmpty { - result = res - } else if let res = niceLocales[getLangFallback(lang)]?[key], !res.isEmpty { - result = res - } else if let res = niceLocales["en"]?[key], !res.isEmpty { - result = res - } else if !key.isEmpty { - result = key - } - - return result + return bundle?.localizedString( + forKey: key, + value: enString, + table: table + ) ?? enString } -public func l(_ key: String, _ locale: String = "en", with args: CVarArg...) -> String { +public func l(_ key: String, _ locale: String? = nil, with args: CVarArg...) -> String { return String(format: l(key, locale), args) } -public func getStringsUrl(_ lang: String) -> String { - return "https://raw.githubusercontent.com/nicegram/translations/master/Telegram-iOS/" + lang + ".lproj/NiceLocalizable.strings" -} - - -var niceWebLocales: [String: [String: String]] = [:] - -func getWebDict(_ lang: String) -> [String : String]? { - return NSDictionary(contentsOf: URL(string: getStringsUrl(lang))!) as? [String : String] -} - -public func downloadLocale(_ locale: String) -> Void { - ngLog("Downloading \(locale)", LOGTAG) - do { - var lang = locale - let rawSuffix = "-raw" - if lang.hasSuffix(rawSuffix) { - lang = String(lang.dropLast(rawSuffix.count)) - } - if let localeDict = try getWebDict(lang) { - niceWebLocales[lang] = localeDict - ngLog("Successfully downloaded locale \(lang)", LOGTAG) - } else { - ngLog("Failed to download \(lang)", LOGTAG) - } - } catch { - return +private func localizationBundle( + code: String +) -> Bundle? { + if let path = Bundle.main.path(forResource: code, ofType: "lproj") { + Bundle(path: path) + } else { + nil } } diff --git a/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift b/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift index 78fff041d48..3994dd2481b 100644 --- a/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift +++ b/Nicegram/NGTranslate/Sources/SpeechToText/TgVoiceToTextProcessor.swift @@ -95,7 +95,7 @@ private extension TgVoiceToTextProcessor { } func makeRecognitionConfig() -> GoogleRecognitionConfig { - let mainCode = Locale.currentAppLocale.languageWithScriptCode + let mainCode = Bundle.main.preferredLocalizations.first ?? "en" var additionalCodes = Set( additionalLanguageCodes + [ Locale.current.languageWithScriptCode, diff --git a/Nicegram/NGUI/BUILD b/Nicegram/NGUI/BUILD index 3490c718244..2511bf14389 100644 --- a/Nicegram/NGUI/BUILD +++ b/Nicegram/NGUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//Nicegram/NGQuickReplies:NGQuickReplies", "//Nicegram/NGSecretMenu:NGSecretMenu", "//Nicegram/NGStats:NGStats", + "//Nicegram/NGStealthMode:NGStealthMode", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAiChatUI", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatImagesHubUI", ], diff --git a/Nicegram/NGUI/Sources/NicegramSettingsController.swift b/Nicegram/NGUI/Sources/NicegramSettingsController.swift index 3727cdcbaf8..484adb34b87 100644 --- a/Nicegram/NGUI/Sources/NicegramSettingsController.swift +++ b/Nicegram/NGUI/Sources/NicegramSettingsController.swift @@ -11,7 +11,6 @@ import AccountContext import Display import FeatImagesHubUI -import FeatPartners import Foundation import ItemListUI import NGData @@ -83,7 +82,6 @@ private enum EasyToggleType { case showRegDate case hideReactions case hideStories - case hidePartnerIntegrations } @@ -534,10 +532,6 @@ private enum NicegramSettingsControllerEntry: ItemListNodeEntry { VarSystemNGSettings.hideReactions = value case .hideStories: NGSettings.hideStories = value - case .hidePartnerIntegrations: - if #available(iOS 13.0, *) { - Partners.hideIntegrations = value - } } }) case let .unblockHeader(text): @@ -640,10 +634,10 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.unblock(l("NicegramSettings.Unblock.Button", locale), nicegramUnblockUrl)) } - entries.append(.TabsHeader(l("NicegramSettings.Tabs", + entries.append(.TabsHeader(l("NiceFeatures.Tabs.Header", locale))) entries.append(.showContactsTab( - l("NicegramSettings.Tabs.showContactsTab", locale), + l("NiceFeatures.Tabs.ShowContacts", locale), NGSettings.showContactsTab )) entries.append(.showCallsTab( @@ -654,18 +648,18 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.showNicegramTab) } entries.append(.showTabNames( - l("NicegramSettings.Tabs.showTabNames", locale), + l("NiceFeatures.Tabs.ShowNames", locale), NGSettings.showTabNames )) - entries.append(.FoldersHeader(l("NicegramSettings.Folders", + entries.append(.FoldersHeader(l("NiceFeatures.Folders.Header", locale))) entries.append(.foldersAtBottom( - l("NicegramSettings.Folders.foldersAtBottom", locale), + l("NiceFeatures.Folders.TgFolders", locale), experimentalSettings.foldersTabAtBottom )) entries.append(.foldersAtBottomNotice( - l("NicegramSettings.Folders.foldersAtBottomNotice", locale) + l("NiceFeatures.Folders.TgFolders.Notice", locale) )) var pinnedBots: [NicegramSettingsControllerEntry] = [] @@ -690,10 +684,10 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat pinnedBots.forEach { entries.append($0) } } - entries.append(.RoundVideosHeader(l("NicegramSettings.RoundVideos", + entries.append(.RoundVideosHeader(l("NiceFeatures.RoundVideos.Header", locale))) entries.append(.startWithRearCam( - l("NicegramSettings.RoundVideos.startWithRearCam", locale), + l("NiceFeatures.RoundVideos.UseRearCamera", locale), NGSettings.useRearCamTelescopy )) entries.append(.shouldDownloadVideo( @@ -704,7 +698,7 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.OtherHeader( presentationData.strings.ChatSettings_Other.uppercased())) entries.append(.hidePhoneInSettings( - l("NicegramSettings.Other.hidePhoneInSettings", locale), + l("NiceFeatures.HideNumber", locale), NGSettings.hidePhoneSettings )) entries.append(.hidePhoneInSettingsNotice( @@ -738,11 +732,6 @@ private func nicegramSettingsControllerEntries(presentationData: PresentationDat entries.append(.easyToggle(toggleIndex, .hideStories, l("NicegramSettings.HideStories", locale), NGSettings.hideStories)) toggleIndex += 1 - if #available(iOS 13.0, *) { - entries.append(.easyToggle(toggleIndex, .hidePartnerIntegrations, Partners.hideIntegrationsTitle, Partners.hideIntegrations)) - toggleIndex += 1 - } - entries.append(.shareChannelsInfoToggle(l("NicegramSettings.ShareChannelsInfoToggle", locale), isShareChannelsInfoEnabled())) entries.append(.shareChannelsInfoNote(l("NicegramSettings.ShareChannelsInfoToggle.Note", locale))) diff --git a/Nicegram/NGUI/Sources/PremiumController.swift b/Nicegram/NGUI/Sources/PremiumController.swift index 17b6e0e2ff3..1bfd09d0c37 100644 --- a/Nicegram/NGUI/Sources/PremiumController.swift +++ b/Nicegram/NGUI/Sources/PremiumController.swift @@ -17,6 +17,7 @@ import ItemListUI import AccountContext import TelegramNotices import NGData +import NGStealthMode import NGStrings private struct SelectionState: Equatable { @@ -45,6 +46,7 @@ private enum premiumControllerSection: Int32 { case notifyMissed case manageFilters case other + case stealthMode case test } @@ -79,6 +81,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { case testButton(PresentationTheme, String) case ignoretr(PresentationTheme, String) + + case stealthMode(Bool) var section: ItemListSectionId { switch self { @@ -94,6 +98,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return premiumControllerSection.other.rawValue case .testButton: return premiumControllerSection.test.rawValue + case .stealthMode: + return premiumControllerSection.stealthMode.rawValue } } @@ -124,6 +130,8 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return 11000 case .ignoretr: return 12000 + case .stealthMode: + return 13000 case .testButton: return 999999 } @@ -217,6 +225,12 @@ private enum PremiumControllerEntry: ItemListNodeEntry { } else { return false } + case let .stealthMode(lhsValue): + if case let .stealthMode(rhsValue) = rhs, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -270,9 +284,12 @@ private enum PremiumControllerEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSetting(value, .rememberFilterOnExit) }) + case let .stealthMode(value): + return ItemListSwitchItem(presentationData: presentationData, title: NGStealthMode.Resources.toggleTitle(), value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + NGStealthMode.setStealthModeEnabled(value) + }) } } - } @@ -286,6 +303,8 @@ private func premiumControllerEntries(presentationData: PresentationData) -> [Pr entries.append(.rememberFolderOnExit(theme, l("Premium.rememberFolderOnExit", locale), NGSettings.rememberFolderOnExit)) entries.append(.onetaptr(theme, l("Premium.OnetapTranslate", locale), NGSettings.oneTapTr)) entries.append(.ignoretr(theme, l("Premium.IgnoreTranslate.Title", locale))) + + entries.append(.stealthMode(NGStealthMode.isStealthModeEnabled())) #if DEBUG entries.append(.testButton(theme, "TEST")) diff --git a/Package.resolved b/Package.resolved index 014bc1c45e2..b7da4a6383d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -42,7 +42,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "develop", - "revision" : "05f2392cbf916a604d9d7e87dc640e7d4651dc12" + "revision" : "b0d993e6fcb6e48734d8361c26bd401752ac5513" } }, { diff --git a/Telegram/BUILD b/Telegram/BUILD index a62a0a95a65..78d9bf21067 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -473,6 +473,7 @@ official_apple_pay_merchants = [ "merchant.org.telegram.billinenet.test", "merchant.org.telegram.billinenet.prod", "merchant.org.telegram.portmone.test", + "merchant.org.telegram.portmone.prod", "merchant.org.telegram.ecommpay.test", ] diff --git a/Telegram/Telegram-iOS/Resources/Celebrate.tgs b/Telegram/Telegram-iOS/Resources/Celebrate.tgs new file mode 100644 index 0000000000000000000000000000000000000000..d1112ecbe732edca653edd932309de28c6393ea6 GIT binary patch literal 14103 zcmV+yH|WS8iwFP!000021MPiVkK9I<=3f!&nMuU`;o6 zxrCBD9;4y^zUMn9f=M#TEM|$Bl`hfUs;Xp?abv_e-?{(u=JP*3-F%~)oBzJ~=Ek?a zH8;)8j}JHB1l8QU`*`yW{ko@LI?*4$-F)MNWhzkm5a4}E`s|KZc;cR&10559f- zmY=x&^y%(1wfp*?H}B}m%@6k)O+9k@|Fki`S+V|Y-#i1{VR;) z(*u9=Gd=Lnn|u7YcuEJYlpv@yDA#sHxj~S)|^ZP}I>U z8UFVlxWmz6n&D#pc>7+K^W*Kq-7lYcy`H|Zsm>FB4OT#xb>Y7TE3BHGS+oYaJYX8H z^cSsxl{Ii>4Sx9b`@2^k-~ICQ@4a;}qjh+goAqm+>>Hk4`MWps*F53wTl_to{eF5$ zuIy~`m|KJ1dQ1O$=TjS9Zg$%FHhafQbMxu=xSHP2{ul?>560W{e)h*cxITJ|s?lnA z8ZI;fT~CwZAw1VGQk)(JMyTRwmHJ!v(@(b_?}Y4OSKj>Nfwtkh&(jxp|0>zP^yE$5 zp4~R#dB1+W)2;WkS5zBge$%|>Q}voZHS%3|QSKbxD9<(h8{hESygu!rX30R_FoU5n z2TgCm)8$}V<077Rq1C+Sy=$E|e95(u2q#^l<&O!MylH*Zbji|P+47~(CWW@FZ57`X zO)IBIwOM<@oUBl`SJY?-`_vB)LRTLDxY$&iI&VduX0i@IfoVW{AE_j2wnaWo7e7q^pFhg<0Ne*1Q0Z%_WGM)&FGgWEpc9$eyK|NY_i zmrp<5KYY0P<4w8re;@9D{m4@(^E;f;SA7I*n*tvJ+nUIm+tF6`xXa~MS>Cu_w+>s` z>Q>23-V++ZC6noovfqP2OE$H^$`SCbat19K>Ls1)2qYDgv)ZMoa0IabZ8kcZ4AN_r z&OyZ-zy!bIBVdjlfj{Mp?9`0jJObB*dv~INK1npNS0)T-+^tM<9IMD_^Zcpo=RzhU}$Ktqrr7={eYsXG_~Zq48I z!3EtpyiuNO`Zo>%&BBM8B?GB|MhD<<)7acbk9&0_h)%GC(22MFneaIsdO8Nt@eMw< z#+oL?)~U=F=nn0gKvFM*bij4%>5+>6HAx9c+XiwmfDXWuPp%=DV{%>z!yE6iGTJy2 zys4WMOmyBPPoGR?0Fza=af9${Lim@l<7ulC2B3YNj44Mz>)6mPRgO0}A#U2wkq8zG zrrL`X5QsH+-q0iisL;@5lQn-PZebc13G2J_Z1`w-#}mjgraAa%d`iZ8XJd*RVt+}9 zJ*|M)%}I#eT_0lal(A7YX?4WzHpKqw;qL9TD%%Tq+iC4j!|OW@H9P?r?b@`#^$o7i zf)`Cp9Xh(!rr3iF790~_^$`c|9Ns9;HM{<;L-4ZrvF7oiboxi=0ilGW&CRWO)5aVd zLwnvkxkB*VGzQRwHHbQDg5h)?5|~@_>Nirq zbO%BDRaA~pF#3>v@<4MM`bPpW*(pC(!f%j$J;?qtc0KJ}ryEPSH(9wNMBn0Zxk4bG z1i(gTIu1szkXY)%zLj99Q67Nqx58pXZFl``ziHaPO57e+!0q-V-0rW|31=P`&p6@~ zn7_aer#t`!)%t>g-vonK-@pI$?un8?=m4S~!AQFYoKq6P6WegrV<)6;af~lJ%A9Bh zfG!9&SJ#GQH@RYyD~=#6PuOHvm4#K4NuL`2YM{xaJBK&QbIq=Q>jA=A_E_`yP%6SQ zu~mtE7-Abm$4NJ2BQ6YH0}RF1n&Q@zOe}pyTnT}`a@@4DfO%21K8bCh%@8AWBxJ>^ zQ9}|1KF8LvZGa>fr*MTtkeu`?7)uvpfHMZh0LKW+M`u0T1~mKo1RIG-Y@)@QqQy(t z_#`-6c0)_FsnMc`!;Tz;U|AS;_y${+jzr=su#v$fha~I3rw76Zc6B02#<2+?*IP14 z*7{64UuCDUy;B|w6T~`2@(2n(Mw@K$f@BM=RuC@SDZ<6YL<{53=-*$x^-u8c&u4HE z+N6qC_q4VDardki2UjJ>Y43g=Ji(qu6CDp}Gc0TZz&s98a4cX%(pPJ;dR;sY_C5=A z-{3ZWsM+;z9AThk4>gYsqVtAGB-lnx`e*5aX|Miz||Ie$xeEaQk)L$0G1l<^trXNL1km~tg zRqi&r(I-O!-D(>OB#5p;0vx_Hg-kR&<;suK`>l#%z>lskVw?PL4?kj_7{?rTMx6GO zH1Y$}#3|U>%xTm0NSU7V7%|tG?Eu5x92ZT@lu)MoGFfeZX9#3?l1dDbNvQ7@$R^!L zVs2BUhv9xHGRhY#@>zk1e@AwR#nvr9Cbmbnbj=W%YCnNmH}&+Up5D~cn|gXvPj4vo zYfw*vIRg6_8wLB22sVd)+_azy~v{13EFM!x|8% z&h9`7l%7Kgw52N8QWb2e3bs@QTdIN$`#ejk0!@2Ru#a;E`#3#^eZpKm*g>fNnV?Uo zRxUq*e6(2w`PfdEWZLA}`ejIdu_vm3f@*d-%$mMVPf)uG@bS~Y$LMDPK3m?^E$`}< zcXi9Vy5(KnpwV^aU1c~R&?r@Cxbu z>m-$s@?sP-uASEio#@@a1~Aey8le!M0~l>i>zmX1=Cr;!t#3~28}@lVr*&e|1^f7_ z7V@)dp)_w2y8!mdM?8Z3D%dC1l;y2b5%w_0$Nd!W;{-NoY9qyKGKW>O*BJcR%^bFw z!!~o+W)9oTVVgPZD>8@aY7Ps!;+`~zd-BTV5l?V~4s)OH3hI1h$#LHWS#_Z345oM#NVWSeP||X*I+{ zhzK)BpwRu8*Y^?`E1-*?7^U!aTb);imvt}iQT=8q-(IYn7D_^gHe}z}64!(1WB+W5#W(H&D18QArOA(uinTL`HMZ!2jixLMw zyB2BEIE;w28G45na;_yr@5Mhu52H4M+iSXJioL)|+IDA4B*H;-VwrBJwLLyH7G@KD@$y z`82a(i1bWa>m+E+GfFd}uwvi_(54HfOC-(Aa;)E0d>h(7c@eu8;lF$jtEV^7mFHaH z6zD@g&00g~{SV9y-v1Z>>cfTaK0n<5eD^W{!*^gl#{t6^@FU^}*@p&_lQ|zhg1|<< z@>v;Ds0>Nv%8*K57=$n+Xb!=U!k8h&D`iMm%#8kAj3~r?u1c5vxTPGA^P~Qx(%iHV zPS^Dj(|8SyyEto$K6LDi6JwQ(9Z`k7IfezctLJT9WZm-p(M#`D&xF~S+x0QGC1vYt zF12~zNv#hA$9b{wEW(`&Z6=iH+GM~>ERs;outg{$Vf@;;HnR5*lKLQ<1BpmEN={iMAI#)Q69{V1zAvA38GO2WrcZ)Xt| zsw@DrI*q5Qx8Q-rdC9x~o<>iflKVQ&(W9p{mi+-`()$kZ_6sCdKJB8CJ!Ua}LW;G0Z>b_n}ChZ*ucd+3nd!88u>5yq*F z_*{Vam=b*eQ4gLDokA12o7-GTkBu8j*-*;4X@OOZ37m@8Hk{#fqXZYE1SJ_Uopr|! zDH3@Ul7n+%E<_@m7&c0(7Kcw8`}Wceu2wk9hHU8BIu@Rvvw;*s0OZA(Ea?f`UYl&Do+#jdYj(Qscw%| zbuwF3r!7Es3y|FcWVZm>EkO3`36KRVmaZA3uvpsy|Xz@^pQOOmql&G0nd?$qwSu*t4@=KtE+;x(zMO| zZ6K<#EZIpNlOd845z7)#HpHxy?POKJrk)3|`6GZ$T^z7&$yT;xD_gRaE!oPJY-NLE z&yj2;b*0gZi@2Vxx2?lW$;pdiu`aWmF>Hn@yt~$wCfSJ4SdQz$u`c4oayeO~(e=x6 zv2^2t*1`G404!#=PC3do;n+S!@MYncn}uTsrNXyzm|Ho_tsLf74s$Dqxxum*UJf&s zoG-Z)!_2dwY^vx1T;DH@%8FZ}C#KUm6XtSpXC2hqeq~HnyQI5wXo71IrMm!;mFKL> zZALbkfV?1c3NpxZP>Q=YBzs{*mYxTZ*?B~kE|17OQT1JwqN|8cpBnyZAmXFS``|`- zuG#f(J>X5t9%~*SN*BLDB79=YO>CnG|8zq(Vz}`dY$TE#QH+^M;!)|3h$|t`SB{$| zQJgMB)%qk#vo-^O>6j3Z#$MqqT8)td=IL`P=_Uxg<+P6DjV@X;XACEPEI!fBdRCg1 zcz7G_=4`euD0>MTpF|GJ#`grd87Wd=rj&yatdp<@NYp!|zp}}d5KRR`B*`Q}e9#EC zaS|26fR1%zx!#gVCedfw`KqLQV285-41;?cW454RS=~_COL90<3&H?$$;3CCmRP6x zC;)R&U=}B%0CGHE5J2rRq*g)i052Q82${ti17L_>>c#}1$t#Qji1B%lnVCmsaSH+1 zLIAc9fGq@I3jx?d0KT3O0B~XkWF;&!&H}R8LJ`k{$3_7F9rRh>|G)N#T#^u@49RBU z6gWnwtVQ5Juyp3aRXYD~fNb;q-+cc!-?Gj3fAjs{(Aw4d{x3#rM$LQvKMPzNdj9vc zXKOqDcgnNero{fEoC9w;TGJUi_C{w^JH1A~{|&2cZvUIx|K|3;x&3c${~Ho}Cb$3P zNKDT-{XdV^Y~=JG&+7CaI|noClHx37)VpXB+<(Fqy8LfQY;*bFT>dwg|IOup zbNSyO*>k)6FNS1h&gK7EFxk-MKc3a)zbeP*@SL_o8jt4Mpv~;`B zvFXwB!Os!@rWSEG;NyFC{oBnq9!YY4D!+R*B2$Sp-62)(%>3NM{KP7IWQO{Y8D4CH zB}wqk%qE;Oeqw?Ll`sdhGmpJL4DurLi@wfpHY&x0iTNGY$r?=1jwg8Lw-oC(oUO27 z;>0W@w&ut@+#@ean+5M_wHJ||iBc?@K&Yg^&Xa=tT%=&x4C*Vev#-FabDVY4#+V9P zS8ir57Rsmw;ZZg&o>ErzVA$Cy>9Zd1MZ@$ZGHy3tYS@|?laiY+*LPHVVHTH;)-~s3 z4qej6nupSu$6a|0x2Fes3at9?0y6qt>@wlxW=E~IF)a#z>#A7qlE6X=pPI zZ7PVDZyMsNj6hfL1(qzGezw$&wR7g7r{XKU7+;xM-p2H5+jOvIiHFVtgjG9XkYwOL zcM`hN0NUpDOXHJD#W=qunS zgaUrTyb7$CC~Rr};r$WJb67L{2rxl@i$iE)Oz}}s$_X>C>YvaC8n1d3n(TYfdJ6m4 z0ru&sV3s#Lg{e}%*-%qH2JvD!p>3PvMC%UkVc&k22-GYY_y9BRmvI1mdKyR34bekK zG>(3_D}Md>_WrkD`p#1BOSVP`aq`XCZf}I)-msC#xY8W1zI>@e=MWXxA!@2C%T83K zs#4civQOw{W;Vr5$$x3{h`b8Nx*%ROST1|o#u={hmu#L<*@GYd=hgSOzr6kQ>fJA& z-n|_<-JK#;1;6*I%R!%%X1Axl?)1%`q!s^oD!1v^dnCW^j@W!4v|w%P07n&maVJ*0u4S`@V-EiiIU@qWq*S|hm*%{^b@-DxqufVWZ63H`3JMjB- z$2x`0grufsM?h9$PIn*z8F?3yZ;g(lUf(Q$sze_Spen!mc@+BF;2P-M`yZG*y#Fu$ z)yJpbeSWzA`R@4CLDJq8C&_>ZUHBCcbOS%~0vBh2o3mWlsdTgBICbUsp>pAz5 zf%X;mMN{Z7RHLOgno|SSftMB$9ocfAH1g08v1W53sDD#CDy)HaG@qqbm()$LqR<83 zTAtI+AcYfM$ce;$TXH}JYeg%YOqcG4n;{P4om{fgkfy`pN>UufA=zT^sdk(;^IbF@ zL(TUz7js8>@Buvgxp-o&^|=gqagel8GL=(}F|q`R`~NoQE&~%~so{rcu2xrR-hurL6$lH(f$HA-1AG9F(+U$cnLu zc6v~O$N=1lnWeYDUqD043$=`wXzY<+xuyMLp(Z!cFL|XWlT&`N{CZW@owEj>qzgK=tN$|f6kilRp%l}3rY}Z_As&i~9Ozig`Uxo4D$eLgG|buOQC!u@&M2Ik8%WfSY38t4)$bsRnrL z=wdED{lpcgRX{;AmrmIq9U}=Hfqg*Q5M_)+3plSZ;i_zA6hD5tPpc!arjEz^#L9u( z5VL3Qiz|UK2T;XxsC=~gx^zDJJaW$pbc(h>aGXmxxV`Q>lib1=b9CeF6yTm3Sy`c_ zZV7UNLQp~SIcLvIvk0=6Jo9Q#w1|%;WJ$K#kB95BWcN03Sdlsvkl2_gIOWbvyMVww z)FDyGB##zC8DaCF4%QY4`jZd?&j6eC!I7z74Cka^3KX(|LEH}1$i_6jkg74UjoINn zC>D%BYeCsGUPU;8jM4rht`31l^p`y%rrqk6NGN3b0KAL95Pr=zmc-@lQMIC zq>N6PBu>{oXEw#aMzo;;jxKPwWziS8${S4tWKZvQ$fXzHzu(v87*@*qwxUrjl7ZV{ zhY7?p!;#f`GVg!})Oh9sJ}+4+Vr-7&A}FME*z@llFlxYcYVi0{DGnYZ>=F7)m53>T z8&R{ovmE*2Q!Hg_5t&D{&D?4&adux4;8o<|Lb%%DSYVJ>`5@|g*c$HvCoZP`~? zz8DSfG^_Jk+Bj1j%L&!6`mL0W3(0{pWT~hXGjPXOtc~+m$WJrld3KJh91~8_V3|?2 zre-Q!HW>eABLw>!(CrQzn)ss6hB+Sev4F68>|Rrv-gRwD)3s&kqNy$KqT<^Pst<@_ zo15-h<`V%1o0!4+j={RZV7UuianNEz0${s~@&q^?Yd9R!i%hUP%N!z`F9HkrL|-IT2IM;(DpdlAtWGVu%90NeT2uR>QWMiw)6|@G8^S9KjKo`X5>J(r#&1TCI)l zq}AG}P$jrVX`kj6oY`~|Hr;XQ!laqDCd1*8I|J)Q$#iZ&aKNji#-9Qa)S|p$)D$8M zrVctMeJ)Nv+UUYQ8g|f8JQV~6^CI#lu;PTE*^UGG5lcY}#5R>93trmon%zmewb>oF zyQ>Xj7zzuB)e)P})T%STPleyxj^F#j?^CIz7?WY=AYBR~5Y1(YPR<75J1crT%O}~R z=uf7v&jS}m>#ea7h35v=eiBU0g1nL}H=EiLZ>s>?RYN1Rj1Z12cWJh3btlc1R(I0u zuC@!&GYEXM+07Ca0ESQiL+${B6Tl#=OVT?nE}T&09qKS5tFbzJu7kfi^$qeVEYK3|fwb69Y`J36X;YH~@9lhM0zt6CZ9reZG76 z{{H?$KYGGH8a?rF+$uH`U_9dlU;L**5#89{<>z9R_m-@2O@YprzS^})!>c*cm+@NA zg%ctr`~r`euF?R!QyF%}1+~_D;X4z3_(AASSrbc;0X^*G*D0YGb>3eRgE6Y!`u**v zzWo}zba~vx>~`zz5faqGn_X(6EX-@R9e8ZpCSv7R)*0bG`KymhG z>#fFx*cO4_z_>c4v-RfdLK*-(0zSr_ud`57HW)^a)M<`q>nwINOER%|zP{2Ww`0EK zcJ%6!+tKR_Zbxq}xE;N{+;;Ttg4@yii*Cno(d`&7xgFC*7cyURA%nW$LI!=oa~;g( z&ULUCJlDZpaytfp(d`&6xgFyrw`02CcDyJFiI*fH@scDYUXX;uLP8=>2zC?`7s>I_ zPt%^sNdA!7JqqzOtR+~@A^LHzWRsqya!%FmWTcz$UzR)IgmeY9Ops9x{XNmE6^kza z__A~bVRC|OJct*{PdA}8aF1)p4kI`%8~&XgxFIPlu411W_D|&U)TyKdZ5`%~c$r~c zVGY-t0PnkTk}qj-SDS=$iDG9JOK&ijWT&_g0?K9Y9dbtFlTP-1h#^n4UgZWJl8zH~ zd4wv#sV}#V4Hme7Y`%fRh1j;o^b&?6BYqVD4k0<0*dOmXoC)?l*7Ikg5Jb!oBjk=5 zQvIWu8r^}>I%_w60lZMQdjmVg4o2N8h;4QUFM!Sw>}r$iFU@t!pNKQ*-r`I1<&b>L&Thn4A zh6!b6^?Q7N+tq9B>ZdxPb{XkVP@8j7_dX;1Wtn?w`crtPu3?$_f4_cy|L&K&SAV_z z;pa>H)Es3Ew3ofX0rAQdAGiPyREyx0o^Pc@dpY(J8+$#c7IqHWrW zaP$ougf1`{A18%@kV7MM)HnMqAi9FH6l7{+Eys3v-66ZHRv~17w-|B?UZrFMjNQg? zl7c{7#y%5)*&)0fyv5`{1ED!RU8WZS6Ecj66H`3+{wb5~h}WJN;Tf)}qRSqxM}Bm< zs>*x!zqxc2)U%`cen9^qqVB%L8zdu9J=sm)Ct>)O_A!8la|H|SY$RevAP-2vic2G> zU&1X$y#9z9WxC)U{E^p0$j2uoe#=DRb2v67ct8S7IYdH;GiU%wrzv@nDqmYx?7`Pe zih>xC{^!vKcM6+?b>xj6Tj z*6ERzvT3kVswF|QiO`ctvR;kA4z5#af~3JB66@(DB0S&A!X(s`NKN$6m)0vur6wWO zZ+Qw%;I@fKro;_o=9iJyCrm)3tEIdz#<_@l`EBm(Rq$J zIuio!)<+*(9;W>snLbkXkFF1`gAGGE&Xo_4^L$Un5ld(*6(@Ncg@@a0d`HANka_>v zBiBMHibYSrYSR#yHSt6g{W5pN(zpKn(8#hVu*YU3iREBbCjrq~BgcW^v=LU0CnQ{>LUk=jis`Nf`@!tvC?D9>0rrU)bgn3e@8>F*Ho% za1N#>?=}FHna%8(H47jRtCQ*Rp}SzobJJSq8zch}phaXSappgt zOU)W+{S4Rb>Z$JFP+0s7M~XjRlZqv2K%?GGL^%PQ#q!MvtqrR0_&D;1gb!D){*t{6 z8AA_55fF*IMi{Aux*}~zAW6CNBQ-Enp~{pl<)c|Zhohw7T2vFBiv!Qb6*}uunm9Ek~T}#97mZ|g4!1I zQTC|yci00=THLu8F&;Tl5Q^n>$)1!Dn*}9c{Tc;LQ8=a4QRCw3C~{W555`E!_wt#m zzZlM+I)#goP$hGUBwe%(tghYFvTa(bm(g8?G-jcnwsdcT&>qL?4#W8;q387DiSF3b z7y8Xqvqt&YWF)|n^)qa43$mJqQ#dP8R3n2l&Q>hd#5iY_qZ(L4#8FDt8>4wGd?=N6xXP?W z2C~d$Lir)$WJHEN!aJr&6b%NsU$NZ5gwC3Wd|8|VlrIDlirBwj9)UU>{>)Ia6dk() zTQiL(9xIPzRPAYqx8&mPfpmpRgYqgV_yc`21`A_q>Bn&@xJdKNtBR1ap^zKP9_|T+ zn646KAvKM#IFedWK3ZC89Mk(I!?7f#t2FY^5<~rdlyPUdI@~;}UdRq2v_5le1T^F5q5?0i9Hv+iRGi= z8VpXyy-^+$$p>a-9v8Ax>vEcCI}?}9`&B05RDJao+>$syelGyZYS&elGQBmoIBvH4 z4!saJ?j5is29Sh?WK$D{$%eO*R-Fll&HL5J!)75%w-Bia-32-s?4=*-SxG51o}i33 z^-7j%jxnYEQ`AgbK!Jjtsc|KH9@h-f^mcaIZ z{CKlKefAIBKrq_#4I?)f7^_POQ_hu&m7`ahrKAA9<%HD)AZcv3W>; z3#DS@IOoU@^0zEBB($xirKCB6c?8ot4<1;X6PG=^8{c6RC((@vt*^-nb*Fv0ef#e2 zm(Qx(` zl0Iqj$k%J&AyKIwQqd+Z##6{F*PHOU1P&X{tc~glr7Ch}vaIW8pQSjCFBzlR*od6@ zg^W7Rph~CyKm&kcqe5bJ84KrZ4pqkh2Z+ptT1@%u;Nxac>bDo`3<8EIW+ZZyb)ctc zf~w%7h&-x63*9u9kxCY@qywm^1d#xVBK>hH-|)dK_gSv>Q)_LQgo3QGS6^6z+Hodr z(-B{X&fvL1)Ke}HH8$)>C>%|rk~3n%X&p*-Vh?u5^sab?qpT*U3U-VL*~;l9o3O1p zm3OGyfZlCrYnEfnMGpcjslHwzM#*SMs3LL%6L7)d7C%4~%Se&^0Aq=XHVDv5;3!5m z6>1wtMJSp2F0O&_CnTS?T|0>i_>mbVP#QTV8ouBf}6p?L1LJ3e|wb@ebS%UP*I1?pMzp06JR zj!LW`pLQRk`jS8#JTpP-*W34sWr+p}*h4wQCUN~(M5=3JY7tihAKKJfTX$oiqlY;8 z6zdg&wb=SXhd3Sr4oxVuQT(_R29r4Zw76fPn5*&}NSi~wKx@LVIn@h5_ylH*AbbKD zz$K6qM9`xP4zh)>5SH))o#7_MR(vDqGi|{JQi(uuLNX9yGQ7x^gkfy7e52)mc3S?t z$e59AWZB5^7DyaDo=6_XwZPwG2FY3IUuHQUG$^L%g}7RT{HBJh?TSi)5H<0x*+vv9WCO6;natBWKHo+Z|FbDonnM3L^2YWhmpclj# ziF4*mYE3u~4Y4KKd_l^|&vFOzVz`6767Imzv;iVk=E;71MsN|{pcgv{Tmv8{5(XA}d$VKIG0(qV5zdm)cQ`CvUW?<77iZF3T zA-JS=JuK_fnq1$5r6ac*`=aa}Y>HX1sR zE_-S(cj}XOFk08d+oAdgZ=Y?vx0A*r`r{}Gjd6LNr)sAqvB6v`HT~|V+qd_>z546@ z=g(Y{=$f4!fhthd5YAZ3T)yB?8l-@n?MwaZd39ZvGa_p%q1CZrnYSV8wz5T1KgF5Q z4;IHD$y|gAz=+)oR~*57RHqDQ^b~wq`{FXtRx8%Lay3oQc0aAe4(M3V!MT<>I##oV zG-`&eL3ha4$eoOG*<{GAAY7uyY$jO?$ry>9M$#0XMWz?)p}TeAC@Fps?u7sHw{O3F z^{;>V+rM2X66ka^==rU)U&35M3`63m=F_BN)0GUWv|RcF$M4od>mr65`YiM7>n zW+5C`Nv9D7cWfEp)6t-0HLpQ}cZS|e(*b2b8`LY6oNmY`3MGiBXXV>n{o3IKa>-6x z$V`Sqahgm+7u9pjCn~P>3L;$s=;6AZQT8&VFPPJ12qiXSC1|i-)xUlB7t*fk!T&h> zJ1vJG?1HkZPsJSKCN9~4!@(ePNEYD*204eQ^l_r-mIG9SN~E;Mb@Hti<-*z|(p)_4 z+?mPYY-WW}T_2;(N6rdKQyC;Ldq0vwtz}gZ-f^fU$h9alt=L)-jR;!kV6+Ny@Hn5k zZ6#$gN{P!ZaeigmH#={f>Sh;@P8q1M4Q4c9k6lN{!OQ5mGPwhI742&IdRM=P8c?&P zn$ab`hSAI~Aav&#d}=*`K!fP@R9=p!#^Bv1=`Wr({uf5ncI=hQ?Ayml+g583T@-aElDM=7|r_yA=uGd>MJOs4Fs?xi#HgvF^#K+ z@N%#S+QHYAV49UJKCXpON=yVfhCVy?6E{NX&p(=sZls$W zHGNoRB8%b5vAAeCaZsrx5!v*mdT(NfD&vfP#e_bwthpe!Ok@Z-F^Y3D+D90nm0asR z0sZ!EGbN1aQk-b1(P@}9Ngs%``h5N*?&ah51K!U$4hPc>=6&pU>TJ}MIs-4 z!U`LNp8>Yxk;u%>yRcSRdLju_`;bY%O&pZ&Q!I zCGGr#$Yz8=*l1=55pCscm=HzguLz^`nhB8^5F&PygS?p2q+}HHA@HQ#RP8%I$jfVoFKpx^TkmuzYpi@;I)vBSDzRanQ(EYC`GM2w_@#yAMXGA z?%`UDHzc+)oPxh=PqprmR{)NLwz%kWa1IF9Fn-@__Hp*sPD*8nYJ#c=u|lTj+8W_4 z*(UbEg_|<6mbb3wqe8Sp!$B0TE9}lmV}V1ctto*`l5#Y`ieVB(avaMpzIe58)f~AX z0aKvEa6(Oqv-hs*i=Mf9=5P@W=SS-n5e^^WU=0=LqMTnr;$3iN880EM{%~L^YX}!& zGhRdIr6yCih6xMLYbc>-U}+dqw}w#T!qKZ+L%IO^+AX1nSF)X5!U-kpw!f~#$3VOb zJo^zS)X;3ihYa`Qr#_W$g*aO@rGnEI!b|$4BSHl zlDJCwwI18-?W!J6M&y9e9AOhhbWgN-+Mft@ypGI|eJLF*nVCZ_5-0}Ou!urE zq3T35dSd(!vuI)gfC$L~O894ZFBef~vIUw=(3VTQp>Q%{YbLL|94D{K--p-pUj|2M@diJnI_xx)#Tm!o z9^Cn4Bn725ygftUki0|k5*&B09e9xcI*`;~?>}D&aoK83Zxz(H3hLXgeo_VX855T= z(GlWQnxl@0Uw9vkcAn#sv~k|4tTP0QuQClY&twi?(oOF^@--5+ztIWQ8brgFdok&^ zZ-pf=YjA}(xYGLXckkcd{|4**$J+F7MkchWdc_~d3+oy) zx#T+!-Bd!YNAP|n(Bbx8M)K3`$GcA_yOyc~a(ezX+9B-HHE`d<>GOz-vr<+;3^2xV zKp%U#v3(8E>+o?Ra+XS71`i(q5`vK|>mY+G{M9%KmKfu9C?RGvJPA41f*E~N& zvq1Et_9fpMhzKTOP@s2MU46kh|BNtWSdXV(hj>Vs zLDD|#k@?>r-hFt5J@e_L)V`S*za!Bt!>SGM{S=s6u)n^MFF5({<7s|Z*@*thi#T{0 z{tNc9<9uhm_wl4=w6Qa7oPHwo#F6l_C%%r1%HM$lt>OarLYY#iOiAU+lu94yT$oZ+ z!j!_8Da9AclzInwV@!WG#uRPGnOLhe{Ak!M&zp>_pc`k}IMefTCVPVD=+ms|sE4BC z_wRodrniRVXr?8{JS{jLB%qR2J)>kuhr}D?Mc1slIS&)&detRnc`Y8?D#xK{ruaX; zNC6RfmN_Hu(^|3mC@Z$h`4JRkc8(|2Q9j0wIka@9$5gv)RrbS0Kn^;0KzF;gus;vm z3!^DyHt5f-M6#~yADepA8nlSr=<3>zqX;uVSE%`R>Z=VSJI$lYyl z#qplg+Glpr7s2%EnSKBL`oq_2pK@dtGina7Den}2PrcJGK6e|>KdUrqkkuARL=!up zpNSE&u_LBcrBP%7YPdW_eyK9)*T@G+2psW2voXQ-nIP+1IS2&IuasS=jL>a-Z{vG^ VK3R8fCVlhA{|7}PTEIU30RV~bMGOD{ literal 0 HcmV?d00001 diff --git a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings index b5236ed3ff5..679d1c22369 100644 --- a/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ar.lproj/NiceLocalizable.strings @@ -1,4 +1,4 @@ -"AppName" = "NiceGram"; +"AppName" = "Nicegram"; /*Common*/ "Nicegram.PrivacyPolicy" = "سياسة الخصوصية"; @@ -112,7 +112,6 @@ "Messages.TranslateError" = "عفوًا، الترجمة غير متوفرة."; "Messages.SpeechToText" = "نص البيان2"; "Messages.UndoSpeechToText" = "إلغاء نص البيان2"; -"Messages.SpeechToText.LowAccuracyError" = "فشل التعرّف على الحديث.\n\nيُمكن أن يحدث ذلك نتيجة لجودة التسجيل الصوتي الرديئة أو بسبب الاختلاف بين لغة التطبيق ولغة التسجيل الصوتي."; "Messages.SelectAllFromUser" = "حدد كافة الرسائل الواردة من هذا المستخدم"; "Messages.ToLanguage" = "إلى اللغة"; "Messages.ToLanguage.WithCode" = "إلى اللغة: %@"; @@ -180,15 +179,6 @@ "NiceFeatures.QuickReplies.AddNew" = "أضف رداً جديداً"; "NiceFeatures.QuickReplies.Placeholder" = "أدخل النص"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "للأسف، لا يمكنك الحصول على**ميزات تيليجرام المُميّز** أثناء تسجيلك الدخول في تطبيق Nicegram. يرجى الانتقال إلى تطبيق تيليجرام الرسمي والاشتراك في **تيليجرام المُميّز**. وعندما تعود إلى تطبيق Nicegram، ستجد جميع **ميزات تيليجرام المُميّز** متاحة للاستخدام."; -"TelegramPremium.Title" = "استعادة اشتراك premium"; -"TelegramPremium.Failure.Title" = "خطأ"; -"TelegramPremium.Failure.Description" = "لقد حدث خطأ ما. يرجى استخدم Nicegram Bot لاستعادة اشتراك Premium Lifetime الخاص بك"; -"TelegramPremium.Failure.Pending" = "طلبك معلق. يرجى الانتظار حتى تتم الموافقة عليه من قبل Nicegram Bot"; -"TelegramPremium.Success.Title" = "تم بنجاح"; -"TelegramPremium.Success.Description" = "تمت استعادة اشتراك Premium Lifetime الخاص بك بنجاح!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "لتمكين هذه الميزة، يجب أن يكون لديك أكثر من حساب وتشغيل قفل رمز المرور (انتقل إلى الإعدادات ← الخصوصية والأمان ← قفل رمز المرور)"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "برنامج مراسلة قوي بتشفير قوي، ومكالمات صوتية ومرئية جماعية، وقنوات عامة، ومجموعات وبرمجيات بوت، وتخزين سحابي غير محدود للمحادثات، ومشاركة الوسائط والمستندات."; "NicegramOnboarding.6.Title" = "تعرف على ليلي"; "NicegramOnboarding.6.Desc" = "مساعد شخصي للتحدث مع الذكاء الاصطناعي وتبسيط حياتك!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "أرسل كفيديو دائري"; +"RoundedVideos.MoreButtonTooltip" = "حول الفيديوهات المربعة لترسل كدوائر بنقرة واحدة."; +"RoundedVideos.SendButtonTooltip" = "اضغط مطولاً لإرسال فيديوك على شكل دائرة أنيقة."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "وضع التخفي"; diff --git a/Telegram/Telegram-iOS/da.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/da.lproj/NiceLocalizable.strings index 02315388bb5..1c3fe85bdee 100644 --- a/Telegram/Telegram-iOS/da.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/da.lproj/NiceLocalizable.strings @@ -96,7 +96,6 @@ "Messages.TranslateError" = "Die Übersetzung ist leider nicht verfügbar."; "Messages.SpeechToText" = "Sprache zu Text"; "Messages.UndoSpeechToText" = "Sprache zu Text rückgängig machen"; -"Messages.SpeechToText.LowAccuracyError" = "Spracherkennung fehlgeschlagen.\n\nDies könnte an der schlechten Qualität der Audioaufnahme oder an Unterschieden zwischen der Sprache der Anwendung und der der Audioaufnahme liegen."; "Messages.SelectAllFromUser" = "Alle von diesem Benutzer auswählen"; "Messages.ToLanguage" = "Zielsprache"; "Messages.ToLanguage.WithCode" = "Zielsprache: %@"; @@ -161,15 +160,6 @@ "NiceFeatures.QuickReplies.AddNew" = "Neue hinzufügen"; "NiceFeatures.QuickReplies.Placeholder" = "Text eingeben"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Leider können Sie **Telegram Premium Features** nicht nutzen, solange Sie in der Nicegram-App eingeloggt sind. Gehen Sie bitte zur offiziellen Telegram-App und abonnieren Sie **Telegram Premium**. Wenn Sie zu Nicegram zurückkehren, stehen Ihnen alle **Premium-Funktionen** zur Verfügung."; -"TelegramPremium.Title" = "Prämie wiederherstellen"; -"TelegramPremium.Failure.Title" = "Fehlschlag"; -"TelegramPremium.Failure.Description" = "Etwas ist schief gelaufen. Bitte verwenden Sie Nicegram Bot, um Ihr Premium-Abonnement auf Lebenszeit wiederherzustellen."; -"TelegramPremium.Failure.Pending" = "Ihre Anfrage ist in Bearbeitung. Bitte warten Sie, bis Nicegram Bot Ihre Anfrage genehmigt hat."; -"TelegramPremium.Success.Title" = "Erfolg"; -"TelegramPremium.Success.Description" = "Ihr Premium-Lifetime-Abonnement wurde erfolgreich wiederhergestellt!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Double Bottom"; "DoubleBottom.Description" = "Um diese Funktion zu aktivieren, sollten Sie mehr als ein Konto haben und die Passcode-Sperre einschalten (gehen Sie zu Einstellungen → Datenschutz & Sicherheit → Passcode-Sperre)"; diff --git a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings index 2c49375cce7..161ade00d49 100644 --- a/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/de.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Die Übersetzung ist leider nicht verfügbar."; "Messages.SpeechToText" = "Sprache zu Text"; "Messages.UndoSpeechToText" = "Sprache zu Text rückgängig machen"; -"Messages.SpeechToText.LowAccuracyError" = "Spracherkennung fehlgeschlagen.\n\nDies könnte an der schlechten Qualität der Audioaufnahme oder an Unterschieden zwischen der Sprache der Anwendung und der der Audioaufnahme liegen."; "Messages.SelectAllFromUser" = "Alle von diesem Benutzer auswählen"; "Messages.ToLanguage" = "Zielsprache"; "Messages.ToLanguage.WithCode" = "Zielsprache: %@"; @@ -180,15 +179,6 @@ "NiceFeatures.QuickReplies.AddNew" = "Neue hinzufügen"; "NiceFeatures.QuickReplies.Placeholder" = "Text eingeben"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Leider können Sie **Telegram Premium Features** nicht nutzen, solange Sie in der Nicegram-App eingeloggt sind. Gehen Sie bitte zur offiziellen Telegram-App und abonnieren Sie **Telegram Premium**. Wenn Sie zu Nicegram zurückkehren, stehen Ihnen alle **Premium-Funktionen** zur Verfügung."; -"TelegramPremium.Title" = "Prämie wiederherstellen"; -"TelegramPremium.Failure.Title" = "Fehlschlag"; -"TelegramPremium.Failure.Description" = "Etwas ist schief gelaufen. Bitte verwenden Sie Nicegram Bot, um Ihr Premium-Abonnement auf Lebenszeit wiederherzustellen."; -"TelegramPremium.Failure.Pending" = "Ihre Anfrage ist in Bearbeitung. Bitte warten Sie, bis Nicegram Bot Ihre Anfrage genehmigt hat."; -"TelegramPremium.Success.Title" = "Erfolg"; -"TelegramPremium.Success.Description" = "Ihr Premium-Lifetime-Abonnement wurde erfolgreich wiederhergestellt!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Double Bottom"; "DoubleBottom.Description" = "Um diese Funktion zu aktivieren, sollten Sie mehr als ein Konto haben und die Passcode-Sperre einschalten (gehen Sie zu Einstellungen → Datenschutz & Sicherheit → Passcode-Sperre)"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Sicherer Messenger mit starker Verschlüsselung, Gruppen-Audio- und Videoanrufe, öffentliche Channels, Gruppen und Bots, unbegrenzter Cloud-Speicher für Chats, Medien- und Dokumentenaustausch."; "NicegramOnboarding.6.Title" = "Triff Lily"; "NicegramOnboarding.6.Desc" = "Dein persönlicher KI-Chatbot-Assistent zur Vereinfachung deines Lebens!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Als rundes Video senden"; +"RoundedVideos.MoreButtonTooltip" = "Quadratische Videos mit einem Tippen in Kreise umwandeln und senden."; +"RoundedVideos.SendButtonTooltip" = "Lange drücken, um Ihr Video als stilvollen Kreis zu senden."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Tarnmodus"; diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f6cb9cb204c..944e98280e7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7906,6 +7906,7 @@ Sorry for the inconvenience."; "Notification.PremiumGift.Title" = "Telegram Premium"; "Notification.PremiumGift.Subtitle" = "for %@"; "Notification.PremiumGift.View" = "View"; +"Notification.PremiumGift.UseGift" = "Use Gift"; "StickerPack.AddEmojiCount_1" = "Add 1 Emoji"; "StickerPack.AddEmojiCount_any" = "Add %@ Emoji"; @@ -8718,6 +8719,7 @@ Sorry for the inconvenience."; "CreateGroup.PublicLinkInfo" = "You can use **a-z**, **0-9** and underscores. Minimum length is **5** characters."; "Notification.RequestedPeer" = "You shared %1$@ with %2$@."; +"Notification.RequestedPeerMultiple" = "You shared %1$@ with %2$@."; "Conversation.ViewInChannel" = "View in Channel"; @@ -9716,6 +9718,7 @@ Sorry for the inconvenience."; "Story.Views.ViewsExpired" = "List of viewers becomes unavailable **24 hours** after the story expires."; "Story.Views.ViewsNotRecorded" = "Information about viewers wasn’t recorded."; "Story.Views.NoViews" = "Nobody has viewed\nyour story yet."; +"Story.Views.NoReactions" = "Nobody has reacted to\nyour story yet."; "AutoDownloadSettings.Stories" = "Stories"; "MediaEditor.Draft" = "Draft"; @@ -10118,10 +10121,6 @@ Sorry for the inconvenience."; "Notification.GiveawayStarted" = "%1$@ just started a giveaway of Telegram Premium subscriptions for its followers."; -"Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@"; -"Channel.AdminLog.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji to %2$@"; -"Channel.AdminLog.MessageChangedBackgroundEmojiRemoved" = "%1$@ removed background emoji"; - "Appearance.NameColor" = "Your Name Color"; "NameColor.Title.Account" = "Your Name Color"; @@ -10224,6 +10223,7 @@ Sorry for the inconvenience."; "ChannelBoost.MoreBoosts.Title" = "More Boosts Needed"; "ChannelBoost.MoreBoosts.Text" = "To boost **%1$@** again, gift **Telegram Premium** to a friend and get **%2$@** additional boosts."; +"ChannelBoost.MoreBoosts.Gift" = "Gift Premium"; "BoostGift.Title" = "Boosts via Gifts"; "BoostGift.Description" = "Get more boosts for your channel by gifting\nPremium to your subscribers."; @@ -10364,6 +10364,7 @@ Sorry for the inconvenience."; "Chat.Giveaway.Info.Won" = "You won a prize in this giveaway. %@"; "Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway."; "Chat.Giveaway.Info.ViewPrize" = "View My Prize"; +"Chat.Giveaway.Info.AdditionalPrizes" = "**%1$@** also included **%2$@** in the prizes. Admins of the channel are responsible for delivering these prizes."; "Chat.Giveaway.Info.FullDate" = "**%1$@** on **%2$@**"; @@ -10390,6 +10391,7 @@ Sorry for the inconvenience."; "Chat.Giveaway.Message.CountriesLastDelimiter" = " and "; "Chat.Giveaway.Message.DateTitle" = "Winners Selection Date"; "Chat.Giveaway.Message.LearnMore" = "LEARN MORE"; +"Chat.Giveaway.Message.With" = "with"; "GiftLink.Title" = "Gift Link"; "GiftLink.UsedTitle" = "Used Gift Link"; @@ -10610,7 +10612,226 @@ Sorry for the inconvenience."; "Chat.RemoveWallpaper.Text" = "Are you sure you want to reset the wallpaper?"; "Chat.RemoveWallpaper.Remove" = "Remove"; +"Paint.CutOut" = "Cut Out"; + "MediaEditor.Shortcut.Image" = "Image"; "MediaEditor.Shortcut.Location" = "Location"; "MediaEditor.Shortcut.Reaction" = "Reaction"; "MediaEditor.Shortcut.Audio" = "Audio"; + +"BoostGift.AdditionalPrizes" = "Additional Prizes"; +"BoostGift.AdditionalPrizesPlaceholder" = "Enter Your Prize"; +"BoostGift.AdditionalPrizesInfoOff" = "Turn this on if you want to give the winners your own prizes in addition to Premium subscriptions."; +"BoostGift.AdditionalPrizesInfoOn" = "All prizes: **%1$@** %2$@ %3$@."; + +"BoostGift.AdditionalPrizesInfoSubscriptions_1" = "%@ Telegram Premium subscription"; +"BoostGift.AdditionalPrizesInfoSubscriptions_any" = "%@ Telegram Premium subscriptions"; + +"BoostGift.AdditionalPrizesInfoWithSubscriptions_1" = "with %@ Telegram Premium subscription"; +"BoostGift.AdditionalPrizesInfoWithSubscriptions_any" = "with %@ Telegram Premium subscriptions"; + +"BoostGift.AdditionalPrizesInfoForMonths_1" = "for **%@** month"; +"BoostGift.AdditionalPrizesInfoForMonths_any" = "for **%@** months"; + +"BoostGift.Winners" = "Show Winners"; +"BoostGift.WinnersInfo" = "Choose whether to make the list of winners public when the giveaway ends."; + +"Story.ViewList.TitleReactions" = "Reactions"; + +"Chat.Giveaway.Message.WinnersSelectedTitle.One" = "Winner Selected!"; +"Chat.Giveaway.Message.WinnersSelectedTitle.Many" = "Winners Selected!"; +"Chat.Giveaway.Message.WinnersSelectedText_1" = "**%@** winner of the [Giveaway]() was randomly selected by Telegram."; +"Chat.Giveaway.Message.WinnersSelectedText_any" = "**%@** winners of the [Giveaway]() were randomly selected by Telegram."; + +"Chat.Giveaway.Message.WinnersTitle.One" = "Winner"; +"Chat.Giveaway.Message.WinnersTitle.Many" = "Winners"; + +"Chat.Giveaway.Message.WinnersMore_1" = "and %@ more!"; +"Chat.Giveaway.Message.WinnersMore_any" = "and %@ more!"; + +"Chat.Giveaway.Message.WinnersInfo.One" = "The winner received their gift link in a private message."; +"Chat.Giveaway.Message.WinnersInfo.Many" = "All winners received gift links in private messages."; + +"Settings.PremiumGift" = "Premium Gifting"; + +"Story.ViewList.ContextSortChannelInfo" = "Choose the order for the list of reactions."; + +"Premium.Gift.MultipleDescription" = "Give %1$@%2$@ access to exclusive features with **Telegram Premium**."; + +"Premium.Gift.NamesAndMore_1" = " and %@ more"; +"Premium.Gift.NamesAndMore_any" = " and %@ more"; + +"Premium.Gift.YouWillReceiveBoosts_1" = "You will receive []() **%@ Boost**!"; +"Premium.Gift.YouWillReceiveBoosts_any" = "You will receive []() **%@ Boosts**!"; + +"Premium.Gift.ContactSelection.Title" = "Gift Premium"; +"Premium.Gift.ContactSelection.Placeholder" = "Search people to gift Premium to..."; +"Premium.Gift.ContactSelection.Proceed" = "Proceed"; +"Premium.Gift.ContactSelection.FrequentContacts" = "FREQUENT CONTACTS"; +"Premium.Gift.ContactSelection.DeselectAll" = "DESELECT ALL"; +"Premium.Gift.ContactSelection.MaximumReached" = "You can select up to %@ users."; + +"Premium.Gift.GiftMultipleSubscriptionsFormat" = "%1$@ for %2$@"; +"Premium.Gift.GiftMultipleSubscriptions_1" = "Gift %@ Subscription"; +"Premium.Gift.GiftMultipleSubscriptions_any" = "Gift %@ Subscriptions"; + +"Message.GiveawayEndedWinners_1" = "%@ winner of the giveaway was randomly selected by Telegram"; +"Message.GiveawayEndedWinners_any" = "%@ winners of the giveaway was randomly selected by Telegram"; +"Message.GiveawayEndedNoWinners" = "Due to the giveaway terms, no winners could be selected by Telegram"; + +"Story.ViewMessage" = "View Message"; + +"Premium.Gift.Terms" = "By gifting Telegram Premium, you agree to the Telegram [Terms of Service](terms) and [Privacy Policy](privacy)."; +"Premium.Gift.Sent.One.Title" = "Gift Sent!"; +"Premium.Gift.Sent.One.Text" = "%@ has been notified about the gift you purchased.\n\nThey now have access to additional features."; +"Premium.Gift.Sent.Multiple.Title" = "Gifts Sent!"; +"Premium.Gift.Sent.Multiple.Text" = "%1$@%2$@ have been notified about the gift you purchased.\n\nThey now have access to additional features."; + +"Premium.Gift.Sent.Close" = "Close"; + +"Premium.Gift.ApplyLink" = "Apply for Free"; +"Premium.Gift.ApplyLink.AlreadyHasPremium.Title" = "You already have Telegram Premium"; +"Premium.Gift.ApplyLink.AlreadyHasPremium.Text" = "You can activate this gift link after **%@** or [send the link]() to a friend."; + +"Premium.Gift.Link.Text" = "This link allows you or [anyone you choose]() to activate a **Telegram Premium** subscription."; +"Premium.Gift.UsedLink.Text" = "This link was used to activate a **Telegram Premium** subscription."; + +"Premium.WhatsIncluded" = "WHAT'S INCLUDED"; + + +"ChannelBoost.Table.Level_1" = "Level %@"; +"ChannelBoost.Table.Level_any" = "Level %@"; + +"ChannelBoost.Table.LevelUnlocks_1" = "Level %@ Unlocks:"; +"ChannelBoost.Table.LevelUnlocks_any" = "Level %@ Unlocks:"; + +"ChannelBoost.Table.StoriesPerDay_1" = "%@ Story Per Day"; +"ChannelBoost.Table.StoriesPerDay_any" = "%@ Stories Per Day"; +"ChannelBoost.Table.CustomReactions_1" = "%@ Custom Reaction"; +"ChannelBoost.Table.CustomReactions_any" = "%@ Custom Reactions"; +"ChannelBoost.Table.NameColor_1" = "%@ Channel Name Color"; +"ChannelBoost.Table.NameColor_any" = "%@ Channel Name Colors"; +"ChannelBoost.Table.ProfileColor_1" = "%@ Color for Channel Cover"; +"ChannelBoost.Table.ProfileColor_any" = "%@ Colors for Channel Cover"; +"ChannelBoost.Table.ProfileLogo" = "Custom Logo for Channel Cover"; +"ChannelBoost.Table.StyleForHeaders_1" = "%@ Style for Links and Quotes"; +"ChannelBoost.Table.StyleForHeaders_any" = "%@ Styles for Links and Quotes"; +"ChannelBoost.Table.HeadersLogo" = "Custom Logo for Links and Quotes"; +"ChannelBoost.Table.EmojiStatus" = "1000+ Emoji Statuses"; +"ChannelBoost.Table.Wallpaper_1" = "%@ Channel Background"; +"ChannelBoost.Table.Wallpaper_any" = "%@ Channel Backgrounds"; +"ChannelBoost.Table.CustomWallpaper" = "Custom Channel Background"; + +"ChannelBoost.AskToBoost" = "Ask your **Premium** subscribers to boost your channel with this link:"; + +"ChannelBoost.NameColor" = "Enable Colors"; +"ChannelBoost.EnableNameColorLevelText" = "Your channel needs **Level %1$@** to change channel color."; + +"ChannelBoost.NameIcon" = "Set Link Icon"; +"ChannelBoost.EnableNameIconLevelText" = "Your channel needs **Level %1$@** to change link icon."; + +"ChannelBoost.ProfileColor" = "Set Cover Color"; +"ChannelBoost.EnableProfileColorLevelText" = "Your channel needs **Level %1$@** to change channel cover color."; + +"ChannelBoost.ProfileIcon" = "Set Cover Icon"; +"ChannelBoost.EnableProfileIconLevelText" = "Your channel needs **Level %1$@** to change channel cover icon."; + +"ChannelBoost.EmojiStatus" = "Use Emoji Statuses"; +"ChannelBoost.EnableEmojiStatusLevelText" = "Your channel needs **Level %1$@** to use emoji statuses."; + +"ChannelBoost.Wallpaper" = "Set Channel Wallpaper"; +"ChannelBoost.EnableWallpaperLevelText" = "Your channel needs **Level %1$@** to set channel wallpaper."; + +"ChannelBoost.CustomWallpaper" = "Set Custom Channel Wallpaper"; +"ChannelBoost.EnableCustomWallpaperLevelText" = "Your channel needs **Level %1$@** to set custom channel wallpaper."; + +"WallpaperPreview.ChannelHeader" = "All subscribers will see this wallpaper"; +"WallpaperPreview.ChannelTopText" = "Details to follow shortly.\nStay tuned!"; +"WallpaperPreview.ChannelReplyText" = "Breaking News"; + +"Wallpaper.ApplyForChannel" = "Apply Wallpaper"; +"Notification.ChannelChangedWallpaper" = "Channel set a new wallpaper"; + +"Story.MessageReposted.Personal" = "Message reposted to your stories."; +"Story.MessageReposted.Channel" = "Message reposted to **%@**."; + +"Story.Views.Commented" = " • commented"; + +"Share.RepostToStory" = "Repost\nto Story"; + +"Conversation.ReadMore" = "Read More"; + +"Wallpaper.ChannelTitle" = "Channel Wallpaper"; +"Wallpaper.ChannelCustomBackgroundInfo" = "Upload your own background image for the channel."; +"Wallpaper.ChannelRemoveBackground" = "Remove Wallpaper"; +"Wallpaper.NoWallpaper" = "No\nWallpaper"; + +"ChatList.PremiumXmasGiftTitle" = "Send gifts to **your friends**! 🎄"; +"ChatList.PremiumXmasGiftText" = "Gift Telegram Premium for Christmas."; + +"ReassignBoost.DescriptionWithLink" = "To boost **%1$@**, reassign a previous boost or [gift Telegram Premium]() to a friend to get **%2$@** additional boosts."; + +"Channel.AdminLog.ChannelChangedNameColorAndIcon" = "%1$@ changed the channel name color and icon to %2$@ %3$@"; +"Channel.AdminLog.ChannelChangedNameColor" = "%1$@ changed the channel name color to %2$@"; +"Channel.AdminLog.ChannelChangedNameIcon" = "%1$@ changed the channel name icon to %2$@"; + +"Channel.AdminLog.ChannelChangedProfileColorAndIcon" = "%1$@ changed the channel profile color and icon to %2$@ %3$@"; +"Channel.AdminLog.ChannelChangedProfileColor" = "%1$@ changed the channel profile color to %2$@"; +"Channel.AdminLog.ChannelChangedProfileIcon" = "%1$@ changed the channel profile icon to %2$@"; +"Channel.AdminLog.ChannelRemovedProfileColorAndIcon" = "%1$@ changed the removed profile color and icon"; +"Channel.AdminLog.ChannelRemovedProfileColor" = "%1$@ removed profile color"; +"Channel.AdminLog.ChannelRemovedProfileIcon" = "%1$@ removed profile icon"; + +"Channel.AdminLog.ChannelUpdatedStatus" = "%1$@ changed the channel status to %2$@"; +"Channel.AdminLog.ChannelRemovedStatus" = "%1$@ removed the channel status"; + +"Channel.AdminLog.ChannelRemovedWallpaper" = "%1$@ removed wallpaper"; + +"Channel.AdminLog.ChannelChangedWallpaper" = "%1$@ set a new wallpaper"; + +"Channel.Appearance.Title" = "Appearance"; +"Channel.Appearance.UnsavedChangesAlertTitle" = "Unsaved Changes"; +"Channel.Appearance.UnsavedChangesAlertText" = "You have changed the channel appearance settings. Apply changes?"; +"Channel.Appearance.UnsavedChangesAlertDiscard" = "Discard"; +"Channel.Appearance.UnsavedChangesAlertApply" = "Apply"; +"Channel.Appearance.ToastAppliedText" = "Appearance settings have been updated."; + +"Channel.Appearance.ExampleReplyText" = "Reply to your channel"; +"Channel.Appearance.ExampleText" = "The color you select will be used for the channel's name"; +"Channel.Appearance.ExampleLinkWebsite" = "Telegram"; +"Channel.Appearance.ExampleLinkTitle" = "Link Preview"; +"Channel.Appearance.ExampleLinkText" = "This preview will also be tinted."; + +"Channel.Appearance.NameIcon" = "Replies Logo"; +"Channel.Appearance.NameColorFooter" = "Choose a color for the name of your channel, the link it sends, and replies to its messages."; + +"Channel.Appearance.Wallpaper" = "Channel Wallpaper"; +"Channel.Appearance.WallpaperFooter" = "Set a wallpaper that will be visible to everyone reading your channel."; + +"Channel.Appearance.ProfileHeader" = "PROFILE PAGE COLOR"; +"Channel.Appearance.ProfileFooter" = "Choose a color and a logo for the channel's profile."; +"Channel.Appearance.ProfileIcon" = "Profile Logo"; +"Channel.Appearance.ResetProfileColor" = "Reset Profile Color"; + +"Channel.Appearance.Status" = "Channel Emoji Status"; +"Channel.Appearance.StatusFooter" = "Choose a status that will be shown next to the channel's name."; + +"Channel.Appearance.ApplyButton" = "Apply Changes"; + +"ChatList.PremiumGiftInSettingsInfo" = "You can gift **Telegram Premium** to a friend later in **Settings**."; + +"Channel.Appearance.BoostLevel" = "Level %@"; + +"Message.FullDateFormat" = "%1$@, %2$@"; + +"SavedMessages.OpenChannel" = "Open Channel"; +"SavedMessages.OpenGroup" = "Open Group"; +"SavedMessages.OpenChat" = "Open Chat"; + +"Channel.Info.BoostLevelPlusBadge" = "Level %@+"; +"Channel.Info.AppearanceItem" = "Appearance"; + +"RequestPeer.SelectUsers" = "Choose Users"; +"RequestPeer.SelectUsers.SearchPlaceholder" = "Search"; +"RequestPeer.ReachedMaximum_1" = "You can select up to %@ user."; +"RequestPeer.ReachedMaximum_any" = "You can select up to %@ users."; diff --git a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings index 34a559543cf..31a858c8037 100644 --- a/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/NiceLocalizable.strings @@ -181,7 +181,7 @@ "NiceFeatures.QuickReplies.Placeholder" = "Enter Text"; /*Telegram Premium*/ -"TelegramPremium.Description" = "Unfortunately, you can’t get **Telegram Premium Features** while being logged in to the Nicegram app. Please go to the official Telegram app and subscribe to **Telegram Premium**. As you go back to Nicegram, all **Premium Features** will be available to use."; +"TelegramPremium.Description" = "Unfortunately, you can’t get Telegram Premium Features while being logged in to the Nicegram app. Please go to the official Telegram app and subscribe to Telegram Premium. As you go back to Nicegram, all Premium Features will be available to use."; /*Double Bottom*/ "DoubleBottom.Title" = "Double bottom"; @@ -205,3 +205,11 @@ "NicegramOnboarding.5.Desc" = "Secure Messenger with Strong Encryption, Group Audio and Video Calls, Public Channels, Groups and Bots, Unlimited Cloud Storage for Chats, Media and Documents sharing."; "NicegramOnboarding.6.Title" = "Meet Lily"; "NicegramOnboarding.6.Desc" = "Your Personal AI Chatbot Assistant for Simplifying Your Life!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Send as Rounded Video"; +"RoundedVideos.MoreButtonTooltip" = "Convert square videos to send as circles with one tap."; +"RoundedVideos.SendButtonTooltip" = "Long-press to send your video as a stylish circle."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Stealth Mode"; diff --git a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings index 390187a692d..272b64ba35b 100644 --- a/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/es.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Lo sentimos, la traducción no está disponible."; "Messages.SpeechToText" = "Voz a texto"; "Messages.UndoSpeechToText" = "Deshacer Voz a texto"; -"Messages.SpeechToText.LowAccuracyError" = "El reconocimiento de voz ha fallado.\n\nEsto podría ocurrir debido a la mala calidad de la grabación de audio o a las diferencias entre el idioma de la aplicación y el de la grabación de audio."; "Messages.SelectAllFromUser" = "Seleccionar todo de este usuario"; "Messages.ToLanguage" = "Al idioma"; "Messages.ToLanguage.WithCode" = "Al idioma: %@"; @@ -180,15 +179,6 @@ "NiceFeatures.QuickReplies.AddNew" = "Agregar nuevo"; "NiceFeatures.QuickReplies.Placeholder" = "Ingrese texto"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Desafortunadamente, no puede obtener **Funciones premium de Telegram** mientras está conectado a la aplicación Nicegram. Vaya a la aplicación oficial de Telegram y suscríbase a **Telegram Premium**. Cuando regrese a Nicegram, todas las **Características Premium** estarán disponibles para su uso."; -"TelegramPremium.Title" = "Restaurar prémium"; -"TelegramPremium.Failure.Title" = "Error"; -"TelegramPremium.Failure.Description" = "Algo salió mal. Utilice Nicegram Bot para restaurar su suscripción prémium de por vida"; -"TelegramPremium.Failure.Pending" = "Su solicitud está en curso. Por favor, espere hasta que Nicegram Bot lo apruebe"; -"TelegramPremium.Success.Title" = "Éxito"; -"TelegramPremium.Success.Description" = "¡Su suscripción prémium de por vida se restauró con éxito!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Para habilitar esta función, debe tener más de una cuenta y activar el Bloqueo con código de acceso (vaya a Configuración → Privacidad y seguridad → Bloqueo con código de acceso)"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Servicio de mensajería seguro con cifrado robusto, llamadas de audio y vídeo grupales, canales públicos, grupos y \"bots\", almacenamiento ilimitado en la nube para chats, multimedia y documentos compartidos."; "NicegramOnboarding.6.Title" = "Conoce a Lily"; "NicegramOnboarding.6.Desc" = "¡Tu chatbot de IA - el asistente personal diseñado para simplificar tu vida!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Enviar como Vídeo Redondeado"; +"RoundedVideos.MoreButtonTooltip" = "Convierte videos cuadrados en círculos para enviar con un solo toque."; +"RoundedVideos.SendButtonTooltip" = "Mantén presionado para enviar tu video como un círculo elegante."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Modo Sigiloso"; diff --git a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings index e1da42aa512..0cbdc87b87f 100644 --- a/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/fr.lproj/NiceLocalizable.strings @@ -40,9 +40,9 @@ "NiceFeatures.Notifications.Fix" = "Désactiver les notifications indésirables"; "NiceFeatures.Notifications.FixNotice" = "Utile si vous recevez des notifications de chats en sourdine.\nLes réponses à partir des notifications, des libellés de compte et des aperçus multimédias ne seront pas disponibles."; "NiceFeatures.Filters.Header" = "FILTRES (ONGLETS)"; -"NiceFeatures.Filters.Notice" = "Sélectionnez le nombre d'onglets personnalisés.\nAppuyez longuement sur l'onglet pour changer de filtre."; +"NiceFeatures.Filters.Notice" = "Sélectionnez le nombre d'onglets personnalisés.\nAppuyez longuement sur l'onglet pour modifier le filtre."; "NiceFeatures.Filters.ShowBadge" = "Afficher les badges (filtres)"; -"NiceFeatures.UseClassicInfoUi" = "Utiliser l'interface utilisateur d'informations de chat classique"; +"NiceFeatures.UseClassicInfoUi" = "Utiliser l'interface utilisateur classique d'informations sur le chat"; /*Common*/ "Common.ExitNow" = "Quitter maintenant"; @@ -79,19 +79,19 @@ "Premium.UnlimitedPins.Header" = "CHATS ÉPINGLÉS ILLIMITÉS"; "Premium.SyncPins" = "Synchroniser les chats épinglés"; "Premium.SyncPins.Notice.ON" = "Si vous modifiez les chats épinglés dans un autre client, ils CHANGERONT dans Nicegram."; -"Premium.SyncPins.Notice.OFF" = "Si vous modifiez les chats épinglés dans un autre client, ils NE CHANGERONT PAS dans Nicegram."; +"Premium.SyncPins.Notice.OFF" = "Si vous modifiez les discussions épinglées dans un autre client, elles NE CHANGERONT PAS dans Nicegram."; "Premium.Missed.Header" = "Messages manqués"; "Premium.Missed" = "Notifier les messages manqués"; -"Premium.Missed.Notice" = "Lorsque vous ouvrez l'application après un long délai (sommeil, révisions, etc.), Nicegram vous informera des messages privés non lus et des mentions."; +"Premium.Missed.Notice" = "Lorsque vous ouvrez l'application après un long délai (dormir, étudier, etc.), Nicegram vous informera des messages privés et des mentions non lus."; "Folder.DeleteAsk" = "Supprimer le dossier"; -"Folder.NeedPremium" = "Ce dossier est uniquement disponible avec Premium. Vous pouvez obtenir Premium ou supprimer un dossier en le faisant glisser sur le côté."; +"Folder.NeedPremium" = "Ce dossier est uniquement disponible avec Premium. Vous pouvez obtenir Premium ou supprimer un dossier par glissement."; "Common.SupportChatUsername" = "nicegramchat"; "Common.FAQUrl" = "https://nicegram.app/faq/"; "Common.FAQ.Button" = "FAQ Nicegram"; -"Common.FAQ.Intro" = "Veuillez noter que l'assistance Nicegram est seulement assurée par le développeur et la communauté.\n\nTout d'abord, jetez un œil à la FAQ Nicegram : elle contient des conseils de dépannage importants et des réponses à la plupart des questions."; +"Common.FAQ.Intro" = "Veuillez noter que le support Nicegram est assuré par le seul développeur et communauté.\n\nTout d’abord, jetez un œil à la FAQ Nicegram : elle contient des conseils de dépannage importants et des réponses à la plupart des questions."; "IAP.Premium.Title" = "Premium"; "IAP.Premium.Subtitle" = "Des fonctionnalités uniques que vous ne pouvez pas refuser !"; -"IAP.Premium.Features" = "Traducteur de message rapide\n\nMémoriser le dossier sélectionné en quittant"; +"IAP.Premium.Features" = "Traducteur de messages rapides\n\nMémoriser le dossier sélectionné à la sortie"; "IAP.Premium.Activated" = "Premium activé !"; "IAP.Common.Restore" = "Restaurer les achats"; "IAP.Common.CantPay" = "Désolé, mais vous ne pouvez pas effectuer d'achats en raison des restrictions de votre appareil ou de votre compte."; @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Désolé, traduction indisponible."; "Messages.SpeechToText" = "Speech2Text"; "Messages.UndoSpeechToText" = "Annuler Speech2Text"; -"Messages.SpeechToText.LowAccuracyError" = "La reconnaissance vocale a échoué.\n\nCela peut être dû à la mauvaise qualité de l'enregistrement audio ou à des différences entre la langue de l'application et celle de l'enregistrement audio."; "Messages.SelectAllFromUser" = "Sélectionner tout de cet utilisateur"; "Messages.ToLanguage" = "Vers la langue"; "Messages.ToLanguage.WithCode" = "Vers la langue : %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "Ajouter nouveau"; "NiceFeatures.QuickReplies.Placeholder" = "Saisissez du texte"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Malheureusement, vous ne pouvez pas obtenir les ** fonctionnalités Premium de Telegram ** lorsque vous êtes connecté à l'application Nicegram. Veuillez vous rendre sur l'application officielle Telegram et vous abonner à **Telegram Premium**. Lorsque vous reviendrez sur Nicegram, toutes les **Fonctionnalités Premium** seront disponibles à l'utilisation."; -"TelegramPremium.Title" = "Restaurer l'abonnement premium"; -"TelegramPremium.Failure.Title" = "Échec"; -"TelegramPremium.Failure.Description" = "Quelque chose s'est mal passé. Veuillez utiliser Nicegram Bot pour restaurer votre abonnement Premium à vie"; -"TelegramPremium.Failure.Pending" = "Votre demande est en attente. S'il vous plaît, attendez que Nicegram Bot l'approuve"; -"TelegramPremium.Success.Title" = "Succès"; -"TelegramPremium.Success.Description" = "Votre abonnement Premium à vie a été restauré avec succès !"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Double fond"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Pour activer cette fonctionnalité, vous devez avoir plus d'un compte et activer le verrouillage par code (allez dans Paramètres → Confidentialité et sécurité → Verrouillage par code)"; -"DoubleBottom.Enabled.Title" = "Le double fond est activé"; -"DoubleBottom.Enabled.Description" = "N'oubliez pas le mot de passe que vous venez de définir et redémarrez l'application pour que le double fond fonctionne correctement"; +"DoubleBottom.Enabled.Title" = "Double Bottom est activé"; +"DoubleBottom.Enabled.Description" = "N'oubliez pas le mot de passe que vous venez de définir et redémarrez l'application pour que le Double Bottom fonctionne correctement"; "DoubleBottom.Enabled.OK" = "D'ACCORD"; "DoubleBottom.Passcode.Error" = "Veuillez définir un autre code d'accès différent de celui que vous utilisez pour le verrouillage par code d'accès"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Messagerie sécurisée avec cryptage fort, appels audio et vidéo de groupe, chaînes publiques, groupes et bots, stockage illimité dans le cloud pour les discussions, partage de médias et de documents."; "NicegramOnboarding.6.Title" = "Rencontrer Lily"; "NicegramOnboarding.6.Desc" = "Votre assistant personnel AI Chatbot pour simplifier votre vie!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Envoyer en tant que vidéo arrondie"; +"RoundedVideos.MoreButtonTooltip" = "Convertissez des vidéos carrées en cercles à envoyer en un seul clic."; +"RoundedVideos.SendButtonTooltip" = "Appuyez longuement pour envoyer votre vidéo sous forme de cercle élégant."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Mode Furtif"; diff --git a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings index 62b1475da3c..31553a24731 100644 --- a/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/it.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Spiacente, traduzione non disponibile."; "Messages.SpeechToText" = "Speech2Text"; "Messages.UndoSpeechToText" = "Annulla Speech2Text"; -"Messages.SpeechToText.LowAccuracyError" = "Riconoscimento vocale fallito.\n\nCiò può essere dovuto a una scarsa qualità della registrazione audio o a differenze tra la lingua dell'applicazione e la lingua della registrazione audio."; "Messages.SelectAllFromUser" = "Seleziona tutto da questo utente"; "Messages.ToLanguage" = "Nella lingua"; "Messages.ToLanguage.WithCode" = "Nella lingua: %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "Aggiungi nuovo"; "NiceFeatures.QuickReplies.Placeholder" = "Inserisci il testo"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Sfortunatamente non puoi ottenere le **Funzionalità di Telegram Premium** tramite Nicegram. Apri l'app ufficiale di Telegram e iscriviti a **Telegram Premium** lì. Torna su Nicegram e tutte le **Funzionalità Premium** saranno disponibili."; -"TelegramPremium.Title" = "Ripristina Premium"; -"TelegramPremium.Failure.Title" = "Errore"; -"TelegramPremium.Failure.Description" = "Qualcosa è andato storto. Per favore, usa il Bot di Nicegram per ripristinare il tuo abbonamento Premium a vita"; -"TelegramPremium.Failure.Pending" = "La tua richiesta è in fase di elaborazione. Per favore, attendi che il Bot di Nicegram la approvi"; -"TelegramPremium.Success.Title" = "Successo"; -"TelegramPremium.Success.Description" = "Il tuo abbonamento Premium a vita è stato ripristinato con successo!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Doppio Fondo"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Per abilitare questa funzione, devi avere più di un account e attivare il Blocco con password (vai su Impostazioni → Privacy e sicurezza → Blocco con password)"; -"DoubleBottom.Enabled.Title" = "Doppio Fondo abilitato"; -"DoubleBottom.Enabled.Description" = "Ricorda la password che hai appena impostato e riavvia l'app affinché il Doppio Fondo funzioni correttamente"; +"DoubleBottom.Enabled.Title" = "Double Bottom abilitato"; +"DoubleBottom.Enabled.Description" = "Ricorda la password che hai appena impostato e riavvia l'app affinché il Double Bottom funzioni correttamente"; "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Si prega di impostare un altro codice di accesso diverso da quello utilizzato per il blocco del codice stesso"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Client di messaggistica sicuro con crittografia avanzata, chiamate audio e video di gruppo, canali pubblici, gruppi e bot, spazio di archiviazione cloud illimitato per le chat, condivisione di file multimediali e documenti."; "NicegramOnboarding.6.Title" = "Conosci Lily"; "NicegramOnboarding.6.Desc" = "Il tuo assistente personale Chatbot d'IA per semplificare la tua vita!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Invia come video rotondo"; +"RoundedVideos.MoreButtonTooltip" = "Converti i video quadrati in cerchi da inviare con un solo tocco."; +"RoundedVideos.SendButtonTooltip" = "Premi a lungo per inviare il tuo video come un cerchio elegante."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Modalità Stealth"; diff --git a/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings index e69de29bb2d..9afc7c4d916 100644 --- a/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/km.lproj/NiceLocalizable.strings @@ -0,0 +1,89 @@ +"AppName" = "Nicegram"; + +/*Common*/ +"Nicegram.PrivacyPolicy" = "គោលកាណ៍ឯកជនភាព"; +"Nicegram.EULA" = "EULA"; +"ChatFilter.Channels" = "កាណាល់"; +"ChatFilter.Private" = "អត្ថជន"; +"ChatFilter.Groups" = "ក្រុម"; +"ChatFilter.Unread" = "មិនបានអាន"; +"ChatFilter.Unmuted" = "មិនស្ងាត់"; +"ChatFilter.Favourites" = "សំណព្វ"; + +/*Nice Features*/ +"NiceFeatures.Title" = "Nicegram"; +"NiceFeatures.Tabs.Header" = "ថេប"; +"NiceFeatures.Tabs.ShowContacts" = "បង្ហាញថេបទំនាក់ទំនង"; +"NiceFeatures.ChatScreen.Header" = "អេក្រង់ឈែត"; +"NiceFeatures.ChatScreen.AnimatedStickers" = "រូបជីវចល"; +"NiceFeatures.useBackCam" = "ប្រើកាមេរ៉ាខាងក្រោយ (រង្វង់វីដេអូ)"; + +"NiceFeatures.Folders.Header" = "ថត"; +"NiceFeatures.Account.Header" = "ការកំណត់គណនី"; +"NiceFeatures.Folders.TgFolders" = "ថតឯបាត"; +"NiceFeatures.Folders.TgFolders.Notice" = "ស្ទាយ៍បញ្ជីថត iOS"; + +"NiceFeatures.RoundVideos.Header" = "រង្វង់វីដេអូ"; +"NiceFeatures.RoundVideos.UseRearCamera" = "ចាប់ផ្ដើមជាមួយកាមេរ៉ាក្រោយ"; + +"NiceFeatures.InstantLock" = "ភ្លាមៗ"; + +/*Save to Cloud*/ +"Chat.SaveToCloud" = "រក្សាទុកទៅពពក"; + +/*Open Pin*/ +"Chat.OpenPin" = "បង្ហាញខ្ទាស់"; +"ChatFilter.Admin" = "មេក្រុម"; +"NiceFeatures.Notifications.Fix" = "វិកលកម្មរំលឹកដែលមិនចង់បាន"; +"NiceFeatures.Notifications.FixNotice" = "មានប្រយោជន៍ បើអ្នកទទួលបានការជូនដំណឹងពីឈែតដែលបានស្ងាត់។ ការឆ្លើយតបពីរម្លឹក ស្លាកគណនី និងការមើលមេឌាជាមុននឹងមិនអាចប្រើបានទេ។"; +"NiceFeatures.Filters.Header" = "តម្រង (ថេប)"; +"NiceFeatures.Filters.Notice" = "ជ្រើសចំនួននៃថេបផ្ទាល់ខ្លួន។\nប៉ះឱ្យយូរ ថេបដើម្បីផ្លាស់តម្រង។"; + +/*Common*/ +"Common.ExitNow" = "ចាកចេញឥឡូវ"; +"Common.Later" = "ពេលក្រោយ"; +"Common.RestartRequired" = "តម្រូវ​ឲ្យ​ចាប់ផ្ដើម​ឡើង​វិញ!"; +"NiceFeatures.Tabs.ShowNames" = "បង្ហាញឈ្មោះថេប"; +"Chat.ForwardAsCopy" = "បញ្ជូនបន្តជាចម្លង"; + +/*Folder*/ +"Folder.DefaultName" = "ថត"; +"Folder.New" = "ថតថ្មី"; +"Folder.Created" = "ថតត្រូវបានបង្កើត"; +"Folder.AddToExisting" = "បន្ថែមទៅថតដែលមានស្រាប់"; +"Folder.Updated" = "ថតត្រូវបានអាប់ដេត"; +"Folder.Create" = "បង្កើតថត…"; +"Folder.Create.Name" = "ឈ្មោះថត"; +"Folder.Create.Placeholder" = "ថត…"; +"Folder.LimitExceeded" = "សូមទោស, អ្នកអាចបង្កើតថតមិនច្រើនជាង ៣ឡើយ។\nថតបន្ថែមអាចប្រើបានក្នុង ព្រីមៀរ។"; +"NiceFeatures.HideNumber" = "លាក់លេខទូរសព្ទក្នុងការកំណត់"; + +/*NGWeb*/ +"NGWeb.Blocked" = "មិនអាចប្រើបានក្នុង Nicegram ពាក់ព័ន្ធនឹងគោលការណ៍ណែនាំហាងកម្មវិធី"; + +/*Browser*/ +/*Please, Go to «Data & Storage» to configure default browser*/ +"NiceFeatures.Use.DataStorage" = "សូមប្រើ «%1» ដើម្បីរៀបរចនារចនាសម្ព័ន្ធច្បោលបុរេជម្រើស"; +"NiceFeatures.Browser.Header" = "URLs"; +"NiceFeatures.Browser.UseBrowser" = "បើកបញ្ជាប់ក្នុងច្បោល"; +"NiceFeatures.Browser.UseBrowserNotice" = "Nicegram នឹងបើកបញ្ជាន់ក្នុងច្បោលក្រៅជំនួសក្នុងកម្មវិធី។ ច្បោលដែលជ្រើសគួរតែបានដំឡើង។"; +"ChatFilter.Missed" = "បានខកខាន"; + +/*Premium*/ +"Premium.Title" = "ព្រីមៀរ"; +"Premium.UnlimitedPins.Header" = "ខ្ទាស់ឈែតដោយឥតកំណត់"; +"Premium.SyncPins" = "កម្មការឈែតដែលខ្ទាស់"; +"Premium.SyncPins.Notice.ON" = "បើអ្នកផ្លាស់ប្ដូរឈែតដែលខ្ទាស់ក្នុងអតិថូបករណ៍ផ្សេង ពួកគេនឹងផ្លាស់ប្ដូរក្នុង Nicegram។"; +"Premium.SyncPins.Notice.OFF" = "បើអ្នកផ្លាស់ប្ដូរឈែតដែលខ្ទាស់ក្នុងអតិថូបករណ៍ផ្សេង ពួកគេនឹងមិនផ្លាស់ប្ដូរក្នុង Nicegram ទេ។"; +"Premium.Missed.Header" = "សារដែលខកខាន"; +"Premium.Missed" = "រម្លឹកសារដែលខកខាន"; +"Premium.Missed.Notice" = "នៅពេលអ្នកបើកកម្មវិធី បន្ទាប់ពីការពន្យារពេលយូរ (គេង សិក្សា។ល។) Nicegram នឹងរម្លឹកអ្នកអំពីសារឯកជន និងប្រារព្ធដែលមិនទាន់អាន។"; +"Folder.DeleteAsk" = "លុបថត"; +"Folder.NeedPremium" = "ថតនេះអាចប្រើបានជាមួយ Premium ប៉ុណ្ណោះ។ អ្នកអាចប្រើព្រីមៀរ ឬលុបថតដោយអូស។"; +"Common.SupportChatUsername" = "nicegramchat"; +"Common.FAQUrl" = "https://nicegram.app/faq/"; +"Common.FAQ.Button" = "Nicegram FAQ"; +"Common.FAQ.Intro" = "សូមចំណាំថា ការគាំទ្រ Nicegram ធ្វើឡើងដោយអ្នកអភិវឌ្ឍន៍ និងសហគមន៍តែមួយគត់។\n\nជាដំបូង សូមក្រឡេកមើល FAQ Nicegram៖ វាមានគន្លឹះដោះស្រាយបញ្ហាសំខាន់ៗ និងចម្លើយចំពោះសំណួរភាគច្រើន។"; +"IAP.Premium.Title" = "ព្រីមៀរ"; +"IAP.Premium.Subtitle" = "លក្ខណៈពិសេសប្លែក ដែលអ្នកមិនអាចបដិសេធ!"; +"IAP.Premium.Features" = "អ្នកបកប្រែសាររហ័ស\n\nចងចាំថតដែលបានជ្រើសនៅពេលចេញ"; diff --git a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings index 9eed236eb9b..f29806e185a 100644 --- a/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ko.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "죄송합니다. 번역 할 수 없습니다."; "Messages.SpeechToText" = "음성-텍스트 변환"; "Messages.UndoSpeechToText" = "음성-텍스트 변환 취소"; -"Messages.SpeechToText.LowAccuracyError" = "음성 인식 실패 \n\n오디오 녹음의 질이 좋지 않거나 앱 프로그램과 오디오 녹음의 언어 차이로 인해 발생할 수 있습니다."; "Messages.SelectAllFromUser" = "이 사용자에게서 온 모든 메시지 선택 \n\n"; "Messages.ToLanguage" = "언어"; "Messages.ToLanguage.WithCode" = "언어: %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "새로 추가"; "NiceFeatures.QuickReplies.Placeholder" = "내용 입력"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "죄송하지만 Nicegram 앱에 로그인된 상태로는 **텔레그램 프리미엄 기능**을 구매하실 수 없습니다. 공식 텔레그램 앱으로 이동해 **텔레그램 프리미엄**을 구독하시기 바랍니다. Nicegram으로 돌아오시면 모든 **프리미엄 기능**을 이용하실 수 있게 됩니다."; -"TelegramPremium.Title" = "프리미엄 구매 복원"; -"TelegramPremium.Failure.Title" = "실패"; -"TelegramPremium.Failure.Description" = "오류가 발생했습니다. Nicegram Bot을 이용해 프리미엄 평생 구독권을 복원 하세요."; -"TelegramPremium.Failure.Pending" = "요청 대기 상태입니다. Nicegram Bot이 승인할 때까지 기다려주세요."; -"TelegramPremium.Success.Title" = "성공"; -"TelegramPremium.Success.Description" = "프리미엄 평생 구독권이 복원 되었습니다!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "이중 바닥"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "이 기능을 활성화하려면 계정이 한 개 이상 필요하며 암호 잠금으로 전환해야 합니다(설정→개인정보 및 보안→암호 잠금 이동)."; -"DoubleBottom.Enabled.Title" = "이중 바닥이 활성화되었습니다."; -"DoubleBottom.Enabled.Description" = "방금 설정한 암호를 기억해두고 이중 바닥이 정상 작동할 수 있도록 앱을 다시 시작하세요."; +"DoubleBottom.Enabled.Title" = "Double Bottom 활성화되었습니다"; +"DoubleBottom.Enabled.Description" = "방금 설정한 암호를 기억해두고 Double Bottom 정상 작동할 수 있도록 앱을 다시 시작하세요"; "DoubleBottom.Enabled.OK" = "확인"; "DoubleBottom.Passcode.Error" = "비밀번호 잠금에 사용하는 비밀번호와 다른 비밀번호를 설정하세요."; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "강력한 암호화, 단체 오디오 및 비디오 통화, 공용 채널, 그룹 및 봇, 채팅과 미디어 및 문서 공유를 위한 무제한 클라우드 스토리지 기능을 갖춘 안전한 메신저입니다."; "NicegramOnboarding.6.Title" = "릴리를 만나보세요"; "NicegramOnboarding.6.Desc" = "생활을 간소화하는 개인 AI 챗봇 비서!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "둥근 비디오로 보내기"; +"RoundedVideos.MoreButtonTooltip" = "한 번의 탭으로 정사각형 비디오를 원형으로 변환하여 보내기."; +"RoundedVideos.SendButtonTooltip" = "길게 눌러 스타일리시한 원형으로 비디오를 보내세요."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "스텔스 모드"; diff --git a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings index b2a03d17c0e..d9afcd38109 100644 --- a/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pl.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Tłumaczenie niedostępne."; "Messages.SpeechToText" = "Mowa na tekst"; "Messages.UndoSpeechToText" = "Cofnij mowę na tekst"; -"Messages.SpeechToText.LowAccuracyError" = "Nie udało się rozpoznać mowy.\n\nMoże to być spowodowane niską jakością nagrania dźwiękowego lub różnicami między językiem aplikacji a językiem nagrania dźwiękowego."; "Messages.SelectAllFromUser" = "Zaznacz wszystkie od tego użytkownika"; "Messages.ToLanguage" = "Na język"; "Messages.ToLanguage.WithCode" = "Na język: %@"; @@ -180,19 +179,10 @@ "NiceFeatures.QuickReplies.AddNew" = "Dodaj nowy"; "NiceFeatures.QuickReplies.Placeholder" = "Wpisz tekst"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Niestety, nie można uzyskać **Telegram Premium Features** będąc zalogowanym do aplikacji Nicegram. Proszę przejdź do oficjalnej aplikacji Telegram i zasubskrybuj **Telegram Premium**. Po powrocie do Nicegram wszystkie **Premium Features** będą dostępne do użytku."; -"TelegramPremium.Title" = "Przywróć Premium"; -"TelegramPremium.Failure.Title" = "Niepowodzenie"; -"TelegramPremium.Failure.Description" = "Coś poszło nie tak. Proszę użyj Nicegram Bota, aby przywrócić swoje dożywotnie subskrypcję Premium"; -"TelegramPremium.Failure.Pending" = "Twoje żądanie jest oczekujące. Proszę czekać, aż Nicegram Bot je zatwierdzi"; -"TelegramPremium.Success.Title" = "Sukces"; -"TelegramPremium.Success.Description" = "Twoja subskrypcja Premium na całe życie została pomyślnie przywrócona!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Podwójny dołek"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Aby włączyć tę funkcję, musisz mieć więcej niż jedno konto i włączyć blokadę kodem dostępu (przejdź do Ustawienia → Prywatność i bezpieczeństwo → Blokada kodem dostępu)"; -"DoubleBottom.Enabled.Title" = "Double Bottom is enabled"; +"DoubleBottom.Enabled.Title" = "Double Bottom jest włączony"; "DoubleBottom.Enabled.Description" = "Pamiętaj hasło, które właśnie ustawiłeś i zrestartuj aplikację, aby Double Dół działał poprawnie"; "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Proszę ustaw inny kod dostępu, różny od tego, którego używasz do blokady kodem dostępu"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Bezpieczny komunikator z silnym szyfrowaniem, grupowym rozmowami audio i wideo, publicznymi kanałami, grupami i botami, nieograniczonym przechowywaniem w chmurze, udostępnianiem czatów, multimediów i dokumentów."; "NicegramOnboarding.6.Title" = "Spotkaj Lily"; "NicegramOnboarding.6.Desc" = "Twój osobisty asystent chatbot SI do upraszczania życia!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Wyślij jako zaokrąglone wideo"; +"RoundedVideos.MoreButtonTooltip" = "Przekształć kwadratowe wideo w koła jednym dotknięciem."; +"RoundedVideos.SendButtonTooltip" = "Przytrzymaj, aby wysłać swoje wideo jako stylowy krąg."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Tryb Ukryty"; diff --git a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings index c32a0d4a2bb..39eb73cd976 100644 --- a/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/pt.lproj/NiceLocalizable.strings @@ -24,9 +24,9 @@ "NiceFeatures.Folders.Header" = "PASTAS"; "NiceFeatures.Account.Header" = "CONFIGURAÇÕES DA CONTA"; "NiceFeatures.Folders.TgFolders" = "Pastas embaixo"; -"NiceFeatures.Folders.TgFolders.Notice" = "lista de pastas com estilo iOS"; +"NiceFeatures.Folders.TgFolders.Notice" = "Lista de pastas com estilo iOS"; -"NiceFeatures.RoundVideos.Header" = "Vídeo mensagens"; +"NiceFeatures.RoundVideos.Header" = "Mensagem video "; "NiceFeatures.RoundVideos.UseRearCamera" = "Iniciar com a câmera traseira"; "NiceFeatures.InstantLock" = "Imediatamente"; @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Desculpe, tradução indisponível."; "Messages.SpeechToText" = "Voz para Texto"; "Messages.UndoSpeechToText" = "Desfazer Voz para Texto"; -"Messages.SpeechToText.LowAccuracyError" = "Falha no reconhecimento de fala.\n\nIsso pode acontecer devido à má qualidade da gravação do áudio ou diferenças entre o idioma do aplicativo e da gravação do áudio."; "Messages.SelectAllFromUser" = "Selecionar tudo deste usuário"; "Messages.ToLanguage" = "Para o idioma"; "Messages.ToLanguage.WithCode" = "Para o idioma: %@"; @@ -180,19 +179,10 @@ "NiceFeatures.QuickReplies.AddNew" = "Adicionar novo"; "NiceFeatures.QuickReplies.Placeholder" = "Inserir texto"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Infelizmente, você não consegue ter acesso aos **Recursos do Telegram Premium** enquanto estiver autenticado no aplicativo Nicegram. Por favor, acesse o aplicativo oficial do Telegram e assine o **Telegram Premium**. Após retornar ao Nicegram, todos os **Recursos Premium** estarão disponíveis para uso."; -"TelegramPremium.Title" = "Restaurar Premium"; -"TelegramPremium.Failure.Title" = "Falha"; -"TelegramPremium.Failure.Description" = "Algo deu errado. Por favor, use o Nicegram Bot para restaurar sua Assinatura Premium Vitalícia"; -"TelegramPremium.Failure.Pending" = "Sua solicitação está pendente. Por favor, aguarde pela aprovação do Nicegram Bot"; -"TelegramPremium.Success.Title" = "Aprovado "; -"TelegramPremium.Success.Description" = "Sua Assinatura Premium Vitalícia foi restaurada com sucesso!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Fundo Duplo"; +"DoubleBottom.Title" = "Double Bottom"; "DoubleBottom.Description" = "Para habilitar esse recurso, você deve ter mais de uma conta e ativar o Bloqueio por Código (acesse Configurações → Privacidade e Segurança → Bloqueio por Código)"; -"DoubleBottom.Enabled.Title" = "O Fundo Duplo está ativado"; +"DoubleBottom.Enabled.Title" = "O Double Bottom está ativado"; "DoubleBottom.Enabled.Description" = "Lembre-se da senha que você acabou de definir e reinicie o aplicativo para que o Fundo Duplo funcione bem"; "DoubleBottom.Enabled.OK" = "OK"; "DoubleBottom.Passcode.Error" = "Por favor, defina outra senha, diferente da que você usa como senha de bloqueio"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Proteja o Aplicativo de Mensagens com Criptografia de Alta Segurança, Chamadas de Áudio e Vídeo em Grupo, Canais Públicos, Grupos e Bots, Armazenamento Ilimitado em Nuvem para as Conversas, compartilhamento de Mídia e Documentos."; "NicegramOnboarding.6.Title" = "Conheça a Lily"; "NicegramOnboarding.6.Desc" = "Seu assistente pessoal de IA Chatbot para simplificar sua vida!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Enviar como Vídeo Arredondado"; +"RoundedVideos.MoreButtonTooltip" = "Converta vídeos quadrados em círculos para enviar com apenas um toque."; +"RoundedVideos.SendButtonTooltip" = "Pressione e segure para enviar seu vídeo como um círculo estiloso."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Modo Furtivo"; diff --git a/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings index 6c0c6492f69..07d2e4b3b8b 100644 --- a/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ro.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Ne pare rău, traducere indisponibilă."; "Messages.SpeechToText" = "Tts"; "Messages.UndoSpeechToText" = "Anulare tts"; -"Messages.SpeechToText.LowAccuracyError" = "Recunoașterea vocală a eșuat.\n\nAcest lucru se poate întâmpla din cauza calității slabe a înregistrării audio sau a diferențelor dintre limba aplicației și cea a înregistrării audio."; "Messages.SelectAllFromUser" = "Selectează toate de la acest utilizator"; "Messages.ToLanguage" = "La Limba"; "Messages.ToLanguage.WithCode" = "La limba: %@"; diff --git a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings index d36211e1bce..ea1b36ccefd 100644 --- a/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/ru.lproj/NiceLocalizable.strings @@ -1,4 +1,4 @@ -"AppName" = "Найсграм"; +"AppName" = "Nicegram"; /*Common*/ "Nicegram.PrivacyPolicy" = "Политика конфиденциальности Nicegram"; @@ -14,7 +14,7 @@ "ChatFilter.Favourites" = "Важные"; /*Nice Features*/ -"NiceFeatures.Title" = "Найсграм"; +"NiceFeatures.Title" = "Nicegram"; "NiceFeatures.Tabs.Header" = "ВКЛАДКИ"; "NiceFeatures.Tabs.ShowContacts" = "Вкладка «Контакты»"; "NiceFeatures.ChatScreen.Header" = "ЧАТ"; @@ -112,7 +112,6 @@ "Messages.TranslateError" = "К сожалению, перевод недоступен."; "Messages.SpeechToText" = "Речь в текст"; "Messages.UndoSpeechToText" = "Отменить «Речь в текст»"; -"Messages.SpeechToText.LowAccuracyError" = "Распознать речь не удалось.\n\nЭто может случаться из-за плохого качества аудио или различий между языком приложения и аудио."; "Messages.SelectAllFromUser" = "Выбрать все от этого пользователя"; "Messages.ToLanguage" = "На язык"; "Messages.ToLanguage.WithCode" = "На язык: %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "Добавить новый"; "NiceFeatures.QuickReplies.Placeholder" = "Введите текст"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "К сожалению, вы не можете получить **функции Telegram Premium**, если вы вошли в приложение Nicegram. Перейдите в официальное приложение Telegram и подпишитесь на **Telegram Premium**. Когда вы вернетесь в Nicegram, все **функции Premium** будут доступны для использования. "; -"TelegramPremium.Title" = "Восстановить премиум-подписку"; -"TelegramPremium.Failure.Title" = "Ошибка"; -"TelegramPremium.Failure.Description" = "Что-то пошло не так. Воспользуйтесь ботом Nicegram, чтобы восстановить пожизненную премиум-подписку"; -"TelegramPremium.Failure.Pending" = "Ваш запрос ожидает рассмотрения. Пожалуйста, подождите, пока бот Nicegram его одобрит"; -"TelegramPremium.Success.Title" = "Готово"; -"TelegramPremium.Success.Description" = "Ваша пожизненная премиум-подписка успешно восстановлена!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Двойное дно"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Чтобы включить эту функцию, у вас должно быть более одного аккаунта и включена блокировка кодом (перейдите в «Настройки» → «Конфиденциальность и безопасность» → «Блокировка кодом»)."; -"DoubleBottom.Enabled.Title" = "Двойное дно включено"; -"DoubleBottom.Enabled.Description" = "Пожалуйста, запомните код доступа, который вы только что установили, и перезапустите приложение, чтобы «Двойное дно» работало правильно"; +"DoubleBottom.Enabled.Title" = "Double Bottom включено"; +"DoubleBottom.Enabled.Description" = "Пожалуйста, запомните код доступа, который вы только что установили, и перезапустите приложение, чтобы «Double Bottom» работало правильно"; "DoubleBottom.Enabled.OK" = "Ок"; "DoubleBottom.Passcode.Error" = "Установите другой код доступа, отличающийся от того, который Вы используете для Блокировки кодом доступа."; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Безопасный мессенджер с надежным шифрованием, групповыми аудио- и видеозвонками, общедоступными каналами, группами и ботами, неограниченным облачным хранилищем для чатов, а также возможностью обмена медиафайлами и документами."; "NicegramOnboarding.6.Title" = "Встречайте ИИ-чатбота Лили!"; "NicegramOnboarding.6.Desc" = "Ваш личный ИИ-помощник, призванный облегчить вам жизнь!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Отправить как круглое видео"; +"RoundedVideos.MoreButtonTooltip" = "Конвертируйте квадратные видео в круглые одним нажатием."; +"RoundedVideos.SendButtonTooltip" = "Удерживайте, чтобы отправить видео в кружке."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Режим инкогнито"; diff --git a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings index c458d3c904e..99c00a37772 100644 --- a/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/tr.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Üzgünüz, çeviri kullanılamıyor."; "Messages.SpeechToText" = "Konuşmadan Metne"; "Messages.UndoSpeechToText" = "Konuşmadan Metne Özelliğini Geri Al"; -"Messages.SpeechToText.LowAccuracyError" = "Konuşma tanıma başarısız oldu.\n\nBu, ses kaydının kalitesizliğinden ya da uygulamanın dili ile ses kaydının dili arasındaki farklılıklardan kaynaklanabilir."; "Messages.SelectAllFromUser" = "Bu Kullanıcıdan Olan Bütün Mesajları Seç"; "Messages.ToLanguage" = "Hedef Dil"; "Messages.ToLanguage.WithCode" = "Hedef Dil: %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "Yeni Ekle"; "NiceFeatures.QuickReplies.Placeholder" = "Metin Gir"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "Ne yazık ki, Nicegram uygulamasında oturum açarken **Telegram Premium Özellikleri**'ni alamazsınız. Lütfen resmi Telegram uygulamasına gidin ve **Telegram Premium**'a abone olun. Nicegram'a geri döndüğünüzde, tüm **Premium Özellikler** kullanıma hazır olacaktır."; -"TelegramPremium.Title" = "Premium'u geri yükleyin"; -"TelegramPremium.Failure.Title" = "Hata"; -"TelegramPremium.Failure.Description" = "Bir şeyler yanlış gitti. Premium Ömür Boyu Üyeliği geri yüklemek için lütfen Nicegram Bot'u kullanın"; -"TelegramPremium.Failure.Pending" = "Talebiniz beklemededir. Nicegram Bot onu onaylayana kadar lütfen bekleyin"; -"TelegramPremium.Success.Title" = "Başarılı"; -"TelegramPremium.Success.Description" = "Premium Ömür Boyu Üyeliğiniz başarılı bir şekilde geri yüklendi!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "Çift Dip"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "Bu özelliği etkinleştirmek için birden fazla hesabınızın olması ve Parola Kilidini açmanız gerekir (Ayarlar → Gizlilik & Güvenlik → Parola Kilidi)"; -"DoubleBottom.Enabled.Title" = "Çift Dip etkinleştirildi"; -"DoubleBottom.Enabled.Description" = "Lütfen az önce belirlediğiniz şifreyi hatırlayın ve Çift Dibin iyi çalışması için uygulamayı yeniden başlatın."; +"DoubleBottom.Enabled.Title" = "Double Bottom etkinleştirildi"; +"DoubleBottom.Enabled.Description" = "Lütfen az önce belirlediğiniz şifreyi hatırlayın ve Double Bottom iyi çalışması için uygulamayı yeniden başlatın"; "DoubleBottom.Enabled.OK" = "TAMAM"; "DoubleBottom.Passcode.Error" = "Lütfen Parola Kilidi için mevcut kullandığınızdan farklı bir parola belirleyin"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "Güçlü Şifreleme, Grup Sesli ve Görüntülü Aramalar, Genel kanallar, Gruplar ve Botlar, Sohbetler Belgeler ve Dökümanlar için Limitsiz Bulut depolaması ile Güvenli Messenger."; "NicegramOnboarding.6.Title" = "Lily ile Tanışın"; "NicegramOnboarding.6.Desc" = "Hayatınızı Basitleştirmek için Kişisel Yapay Zeka Sohbet Robotu Asistanınız!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "Yuvarlak Video Olarak Gönder"; +"RoundedVideos.MoreButtonTooltip" = "Kare videoları tek dokunuşla daire olarak göndermek için dönüştürün."; +"RoundedVideos.SendButtonTooltip" = "Videonuzu şık bir daire olarak göndermek için uzun basın."; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "Gizli Mod"; diff --git a/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings index 0f223222c86..11421761fb2 100644 --- a/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/uk.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "На жаль, переклад недоступний."; "Messages.SpeechToText" = "Голос в текст"; "Messages.UndoSpeechToText" = "Скасувати голос у текст"; -"Messages.SpeechToText.LowAccuracyError" = "Помилка розпізнавання мовлення.\n\nЦе може статися через погану якість аудіозапису або відмінності між мовою програми та аудіозапису."; "Messages.SelectAllFromUser" = "Вибрати все цього користувача"; "Messages.ToLanguage" = "На мову"; "Messages.ToLanguage.WithCode" = "На мову: %@"; @@ -179,15 +178,6 @@ "NiceFeatures.QuickReplies.AddNew" = "Додати нову"; "NiceFeatures.QuickReplies.Placeholder" = "Введіть текст"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "На жаль, ви не можете отримати **Преміум можливості Telegram** під час входу в додаток Nicegram. Перейдіть в офіційний додаток Telegram і підпишіться на **Telegram Premium**. Коли ви повернетесь до Nicegram, усі **Преміум можливості будуть доступні для використання."; -"TelegramPremium.Title" = "Відновити преміум-версію"; -"TelegramPremium.Failure.Title" = "Помилка"; -"TelegramPremium.Failure.Description" = "Щось пішло не так. Будь ласка, використовуйте Nicegram Bot, щоб відновити вашу довічну Преміум підписку"; -"TelegramPremium.Failure.Pending" = "Ваш запит очікує очікування. Будь ласка, почекайте поки Nicegram Bot схвалить його"; -"TelegramPremium.Success.Title" = "Успішно"; -"TelegramPremium.Success.Description" = "Ваша пожиттєва Преміум підписка була успішно відновлена!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Подвійне дно"; "DoubleBottom.Description" = "Щоб увімкнути цю функцію, ви маєте мати більше одного облікового запису і перемикатись на пароль (перейдіть у Параметри → Приватність і Безпека → Пароль блокування)"; diff --git a/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings index 17876d3bffd..449d34446dc 100644 --- a/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/vi.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "Sorry, translation unavailable."; "Messages.SpeechToText" = "Speech2Text"; "Messages.UndoSpeechToText" = "Undo Speech2Text"; -"Messages.SpeechToText.LowAccuracyError" = "Speech recognition failed.\n\nThis could happen due to the poor quality of the audio recording or differences between the language of the application and of the audio recording."; "Messages.SelectAllFromUser" = "Select All From This User"; "Messages.ToLanguage" = "To Language"; "Messages.ToLanguage.WithCode" = "To Language: %@"; diff --git a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings index 07601c7b497..31a135f90ec 100644 --- a/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hans.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "抱歉,翻译不可用。"; "Messages.SpeechToText" = "语音转文本"; "Messages.UndoSpeechToText" = "撤销语音转文本"; -"Messages.SpeechToText.LowAccuracyError" = "语音识别失败。\n\n这可能是由于录音质量太差或应用程序的语言与录音中的语言不同造成的。"; "Messages.SelectAllFromUser" = "选择来自该用户的所有消息"; "Messages.ToLanguage" = "为语言"; "Messages.ToLanguage.WithCode" = "为语言: %@"; @@ -180,20 +179,11 @@ "NiceFeatures.QuickReplies.AddNew" = "新增"; "NiceFeatures.QuickReplies.Placeholder" = "输入文本"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "遗憾的是,您无法在登录 Nicegram 应用后获取 **Telegram 高级功能**。请前往官方 Telegram 应用并订阅 **Telegram 高级版**。在您返回 Nicegram 后,所有**高级功能**将可用。"; -"TelegramPremium.Title" = "恢复高级版"; -"TelegramPremium.Failure.Title" = "失败"; -"TelegramPremium.Failure.Description" = "出现了问题。请使用 Nicegram 机器人恢复你的高级版终身订阅。"; -"TelegramPremium.Failure.Pending" = "你的请求待处理。请等待 Nicegram 机器人批准它"; -"TelegramPremium.Success.Title" = "成功"; -"TelegramPremium.Success.Description" = "你的高级版终身订阅已成功恢复!"; - /*Double Bottom*/ -"DoubleBottom.Title" = "双底"; +"DoubleBottom.Title" = "Double bottom"; "DoubleBottom.Description" = "要启用此功能,你需有超过一个账户,并将密码锁切换到打开状态( 转向设置 → 隐私与安全 → 密码锁)"; -"DoubleBottom.Enabled.Title" = "双底已启用"; -"DoubleBottom.Enabled.Description" = "请记住你刚设置的密码,并重启应用,以便“双底”可以正常运作"; +"DoubleBottom.Enabled.Title" = "Double bottom已启用"; +"DoubleBottom.Enabled.Description" = "请记住你刚设置的密码,并重启应用,以便“Double Bottom”可以正常运作"; "DoubleBottom.Enabled.OK" = "确认"; "DoubleBottom.Passcode.Error" = "请设置另一个与密码锁不同的密码"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "安全的通讯工具,拥有强加密、群组音频和视频电话、公共频道、群聊和机器人、无限制的聊天云存储空间、媒体和文档共享功能。"; "NicegramOnboarding.6.Title" = "认识莉莉"; "NicegramOnboarding.6.Desc" = "您的个人人工智能聊天机器人助手,简化您的生活!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "以圆形视频发送"; +"RoundedVideos.MoreButtonTooltip" = "一键将方形视频转换为圆形发送。"; +"RoundedVideos.SendButtonTooltip" = "长按以将您的视频以时尚圆形发送。"; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "隐身模式"; diff --git a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings index ef9bacd53b6..31be16b4e24 100644 --- a/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings +++ b/Telegram/Telegram-iOS/zh-hant.lproj/NiceLocalizable.strings @@ -112,7 +112,6 @@ "Messages.TranslateError" = "抱歉,無法翻譯。"; "Messages.SpeechToText" = "語音轉文字"; "Messages.UndoSpeechToText" = "取消語音轉文字"; -"Messages.SpeechToText.LowAccuracyError" = "語音辨識失敗。\n\n這可能是由於錄音品質不好,或應用程式和錄音語言之間的差異。"; "Messages.SelectAllFromUser" = "選取來自這個人的所有訊息"; "Messages.ToLanguage" = "翻譯為"; "Messages.ToLanguage.WithCode" = "翻譯為:%@"; @@ -180,15 +179,6 @@ "NiceFeatures.QuickReplies.AddNew" = "新增"; "NiceFeatures.QuickReplies.Placeholder" = "輸入文字"; -/*Telegram Premium*/ -"TelegramPremium.Description" = "很遺憾地,當你登入 Nicegram App 時,你沒有辦法取得**Telegram Premium**。\n請到官方的 Telegram App 並訂閱** Telegram Premium**。當你再度回到 Nicegram 時,你將可以使用所有的**Premium 功能**。"; -"TelegramPremium.Title" = "恢復 Premium"; -"TelegramPremium.Failure.Title" = "失敗"; -"TelegramPremium.Failure.Description" = "出了些問題。 請使用 Nicegram Bot 恢復您的 Premium 終身訂閱"; -"TelegramPremium.Failure.Pending" = "您的要求正待處理。 請等待直到 Nicegram Bot 核准之"; -"TelegramPremium.Success.Title" = "成功"; -"TelegramPremium.Success.Description" = "您的終生 Premium 已成功恢復了!"; - /*Double Bottom*/ "DoubleBottom.Title" = "Double Bottom"; "DoubleBottom.Description" = "要啟用此功能,您必須擁有一個以上的帳號並啟用密碼鎖 (前往設定→隱私與安全→密碼鎖)"; @@ -211,3 +201,11 @@ "NicegramOnboarding.5.Desc" = "具有高強度加密、群組語音及視訊通話、公用頻道、群組和機器人、可用於聊天、媒體和文件共享的無限雲端儲存之安全即時通訊程式。"; "NicegramOnboarding.6.Title" = "认识莉莉"; "NicegramOnboarding.6.Desc" = "您的个人人工智能聊天机器人助手,简化您的生活!"; + +/*Rounded Videos*/ +"RoundedVideos.ButtonTitle" = "以圓形影片發送"; +"RoundedVideos.MoreButtonTooltip" = "一鍵將方形影片轉換為圓形發送。"; +"RoundedVideos.SendButtonTooltip" = "長按以將您的影片以時尚圓形發送。"; + +/*Stealth Mode*/ +"StealthMode.Toggle" = "隱形模式"; diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index c17d759f733..7641bd93e7b 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -29,7 +29,8 @@ public final class ViewController: UIViewController { shortName: "Emma", avatarImage: UIImage(named: "test"), audioOutput: .internalSpeaker, - isMicrophoneMuted: false, + isLocalAudioMuted: false, + isRemoteAudioMuted: false, localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false @@ -60,11 +61,11 @@ public final class ViewController: UIViewController { } switch self.callState.lifecycleState { - case .connecting: + case .requesting: self.callState.lifecycleState = .ringing case .ringing: - self.callState.lifecycleState = .exchangingKeys - case .exchangingKeys: + self.callState.lifecycleState = .connecting + case .connecting: self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: Date().timeIntervalSince1970, signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0), @@ -73,6 +74,12 @@ public final class ViewController: UIViewController { case var .active(activeState): activeState.signalInfo.quality = activeState.signalInfo.quality == 1.0 ? 0.1 : 1.0 self.callState.lifecycleState = .active(activeState) + case .reconnecting: + self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState( + startTime: Date().timeIntervalSince1970, + signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0), + emojiKey: ["😂", "😘", "😍", "😊"] + )) case .terminated: self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: Date().timeIntervalSince1970, @@ -112,8 +119,8 @@ public final class ViewController: UIViewController { } if let input = self.callState.localVideo as? FileVideoSource { input.sourceId = input.sourceId == 0 ? 1 : 0 - input.fixedRotationAngle = input.sourceId == 0 ? Float.pi * 0.0 : Float.pi * 0.5 - input.sizeMultiplicator = input.sourceId == 0 ? CGPoint(x: 1.0, y: 1.0) : CGPoint(x: 1.5, y: 1.0) + //input.fixedRotationAngle = input.sourceId == 0 ? Float.pi * 0.5 : Float.pi * 0.5 + //input.sizeMultiplicator = input.sourceId == 0 ? CGPoint(x: 1.0, y: 1.0) : CGPoint(x: 1.0, y: 0.5) } } callScreenView.videoAction = { [weak self] in @@ -121,7 +128,7 @@ public final class ViewController: UIViewController { return } if self.callState.localVideo == nil { - self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test3", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0) + self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test3", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.5) } else { self.callState.localVideo = nil } @@ -129,7 +136,7 @@ public final class ViewController: UIViewController { } callScreenView.microhoneMuteAction = { if self.callState.remoteVideo == nil { - self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test4", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0) + self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test4", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 1.0) } else { self.callState.remoteVideo = nil } @@ -139,33 +146,41 @@ public final class ViewController: UIViewController { guard let self else { return } - self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0)) + self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0, reason: .hangUp)) self.callState.remoteVideo = nil self.callState.localVideo = nil + self.callState.isLocalAudioMuted = false + self.callState.isRemoteBatteryLow = false self.update(transition: .spring(duration: 0.4)) } callScreenView.backAction = { [weak self] in guard let self else { return } - self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted + self.callState.isLocalAudioMuted = !self.callState.isLocalAudioMuted self.update(transition: .spring(duration: 0.4)) } + callScreenView.closeAction = { [weak self] in + guard let self else { + return + } + self.callScreenView?.speakerAction?() + } } private func update(transition: Transition) { if let (size, insets) = self.currentLayout { - self.update(size: size, insets: insets, transition: transition) + self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition) } } - private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) { + private func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, transition: Transition) { guard let callScreenView = self.callScreenView else { return } transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size)) - callScreenView.update(size: size, insets: insets, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition) + callScreenView.update(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition) } override public func viewWillLayoutSubviews() { @@ -182,7 +197,7 @@ public final class ViewController: UIViewController { if let currentLayout = self.currentLayout, currentLayout == (size, insets) { } else { self.currentLayout = (size, insets) - self.update(size: size, insets: insets, transition: transition) + self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition) } } diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f9016422101..c75999e082d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -129,7 +129,7 @@ public final class AccountWithInfo: Equatable { public enum OpenURLContext { case generic - case chat(peerId: PeerId, updatedPresentationData: (initial: PresentationData, signal: Signal)?) + case chat(peerId: PeerId, message: Message?, updatedPresentationData: (initial: PresentationData, signal: Signal)?) } public struct ChatAvailableMessageActionOptions: OptionSet { @@ -903,6 +903,7 @@ public protocol SharedAccountContext: AnyObject { ) -> ChatHistoryListNode func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader + func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? func makeContactSelectionController(_ params: ContactSelectionControllerParams) -> ContactSelectionController func makeContactMultiselectionController(_ params: ContactMultiselectionControllerParams) -> ContactMultiselectionController @@ -946,6 +947,7 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController + func makePremiumGiftController(context: AccountContext) -> ViewController func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController @@ -1107,7 +1109,22 @@ public protocol AccountContext: AnyObject { public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { - return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2) + return PremiumConfiguration( + isPremiumDisabled: false, + showPremiumGiftInAttachMenu: false, + showPremiumGiftInTextField: false, + giveawayGiftsPurchaseAvailable: false, + boostsPerGiftCount: 3, + audioTransciptionTrialMaxDuration: 300, + audioTransciptionTrialCount: 2, + minChannelNameColorLevel: 1, + minChannelNameIconLevel: 4, + minChannelProfileColorLevel: 5, + minChannelProfileIconLevel: 7, + minChannelEmojiStatusLevel: 8, + minChannelWallpaperLevel: 9, + minChannelCustomWallpaperLevel: 10 + ) } public let isPremiumDisabled: Bool @@ -1115,35 +1132,73 @@ public struct PremiumConfiguration { public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool public let boostsPerGiftCount: Int32 - public let minChannelNameColorLevel: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 + public let minChannelNameColorLevel: Int32 + public let minChannelNameIconLevel: Int32 + public let minChannelProfileColorLevel: Int32 + public let minChannelProfileIconLevel: Int32 + public let minChannelEmojiStatusLevel: Int32 + public let minChannelWallpaperLevel: Int32 + public let minChannelCustomWallpaperLevel: Int32 - fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32) { + fileprivate init( + isPremiumDisabled: Bool, + showPremiumGiftInAttachMenu: Bool, + showPremiumGiftInTextField: Bool, + giveawayGiftsPurchaseAvailable: Bool, + boostsPerGiftCount: Int32, + audioTransciptionTrialMaxDuration: Int32, + audioTransciptionTrialCount: Int32, + minChannelNameColorLevel: Int32, + minChannelNameIconLevel: Int32, + minChannelProfileColorLevel: Int32, + minChannelProfileIconLevel: Int32, + minChannelEmojiStatusLevel: Int32, + minChannelWallpaperLevel: Int32, + minChannelCustomWallpaperLevel: Int32 + + ) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.boostsPerGiftCount = boostsPerGiftCount - self.minChannelNameColorLevel = minChannelNameColorLevel self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount + self.minChannelNameColorLevel = minChannelNameColorLevel + self.minChannelNameIconLevel = minChannelNameIconLevel + self.minChannelProfileColorLevel = minChannelProfileColorLevel + self.minChannelProfileIconLevel = minChannelProfileIconLevel + self.minChannelEmojiStatusLevel = minChannelEmojiStatusLevel + self.minChannelWallpaperLevel = minChannelWallpaperLevel + self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { + let defaultValue = self.defaultValue if let data = appConfiguration.data { + func get(_ value: Any?) -> Int32? { + return (value as? Double).flatMap(Int32.init) + } return PremiumConfiguration( - isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? false, - showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false, - showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false, - giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false, - boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3), - minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5), - audioTransciptionTrialMaxDuration: Int32(data["transcribe_audio_trial_duration_max"] as? Double ?? 300), - audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2) + isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? defaultValue.isPremiumDisabled, + showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, + showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, + giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, + boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, + audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, + audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, + minChannelNameColorLevel: get(data["channel_color_level_min"]) ?? defaultValue.minChannelNameColorLevel, + minChannelNameIconLevel: get(data["channel_bg_icon_level_min"]) ?? defaultValue.minChannelNameIconLevel, + minChannelProfileColorLevel: get(data["channel_profile_color_level_min"]) ?? defaultValue.minChannelProfileColorLevel, + minChannelProfileIconLevel: get(data["channel_profile_bg_icon_level_min"]) ?? defaultValue.minChannelProfileIconLevel, + minChannelEmojiStatusLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelEmojiStatusLevel, + minChannelWallpaperLevel: get(data["channel_wallpaper_level_min"]) ?? defaultValue.minChannelWallpaperLevel, + minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel ) } else { - return .defaultValue + return defaultValue } } } @@ -1286,19 +1341,19 @@ public class PeerNameColors: Equatable { public let secondary: UIColor? public let tertiary: UIColor? - init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) { + public init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) { self.main = main self.secondary = secondary self.tertiary = tertiary } - init(main: UIColor) { + public init(main: UIColor) { self.main = main self.secondary = nil self.tertiary = nil } - init?(colors: [UIColor]) { + public init?(colors: [UIColor]) { guard let first = colors.first else { return nil } @@ -1339,7 +1394,8 @@ public class PeerNameColors: Equatable { profilePaletteDarkColors: [:], profileStoryColors: [:], profileStoryDarkColors: [:], - profileDisplayOrder: [] + profileDisplayOrder: [], + nameColorsChannelMinRequiredBoostLevel: [:] ) } @@ -1355,6 +1411,8 @@ public class PeerNameColors: Equatable { public let profileStoryDarkColors: [Int32: Colors] public let profileDisplayOrder: [Int32] + public let nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] + public func get(_ color: PeerNameColor, dark: Bool = false) -> Colors { if dark, let colors = self.darkColors[color.rawValue] { return colors @@ -1404,7 +1462,8 @@ public class PeerNameColors: Equatable { profilePaletteDarkColors: [Int32: Colors], profileStoryColors: [Int32: Colors], profileStoryDarkColors: [Int32: Colors], - profileDisplayOrder: [Int32] + profileDisplayOrder: [Int32], + nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] ) { self.colors = colors self.darkColors = darkColors @@ -1416,6 +1475,7 @@ public class PeerNameColors: Equatable { self.profileStoryColors = profileStoryColors self.profileStoryDarkColors = profileStoryDarkColors self.profileDisplayOrder = profileDisplayOrder + self.nameColorsChannelMinRequiredBoostLevel = nameColorsChannelMinRequiredBoostLevel } public static func with(availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions) -> PeerNameColors { @@ -1430,8 +1490,14 @@ public class PeerNameColors: Equatable { var profileStoryDarkColors: [Int32: Colors] = [:] var profileDisplayOrder: [Int32] = [] + var nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] = [:] + if !availableReplyColors.options.isEmpty { for option in availableReplyColors.options { + if let requiredChannelMinBoostLevel = option.value.requiredChannelMinBoostLevel { + nameColorsChannelMinRequiredBoostLevel[option.key] = requiredChannelMinBoostLevel + } + if let parsedLight = PeerNameColors.Colors(colors: option.value.light.background) { colors[option.key] = parsedLight } @@ -1490,7 +1556,8 @@ public class PeerNameColors: Equatable { profilePaletteDarkColors: profilePaletteDarkColors, profileStoryColors: profileStoryColors, profileStoryDarkColors: profileStoryDarkColors, - profileDisplayOrder: profileDisplayOrder + profileDisplayOrder: profileDisplayOrder, + nameColorsChannelMinRequiredBoostLevel: nameColorsChannelMinRequiredBoostLevel ) } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 7e8a7d5b80c..d434b333eac 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -52,6 +52,7 @@ public final class ChatMessageItemAssociatedData: Equatable { public let maxReadStoryId: Int32? public let recommendedChannels: RecommendedChannels? public let audioTranscriptionTrial: AudioTranscription.TrialState + public let chatThemes: [TelegramTheme] public init( automaticDownloadPeerType: MediaAutoDownloadPeerType, @@ -77,7 +78,8 @@ public final class ChatMessageItemAssociatedData: Equatable { translateToLanguage: String? = nil, maxReadStoryId: Int32? = nil, recommendedChannels: RecommendedChannels? = nil, - audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue + audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue, + chatThemes: [TelegramTheme] = [] ) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId @@ -103,6 +105,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.maxReadStoryId = maxReadStoryId self.recommendedChannels = recommendedChannels self.audioTranscriptionTrial = audioTranscriptionTrial + self.chatThemes = chatThemes } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -175,6 +178,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.audioTranscriptionTrial != rhs.audioTranscriptionTrial { return false } + if lhs.chatThemes != rhs.chatThemes { + return false + } return true } } diff --git a/submodules/AccountContext/Sources/ChatListController.swift b/submodules/AccountContext/Sources/ChatListController.swift index 0ff4cdd4b20..99ee3583284 100644 --- a/submodules/AccountContext/Sources/ChatListController.swift +++ b/submodules/AccountContext/Sources/ChatListController.swift @@ -6,6 +6,7 @@ import TelegramCore public enum ChatListControllerLocation: Equatable { case chatList(groupId: EngineChatList.Group) case forum(peerId: EnginePeer.Id) + case savedMessagesChats } public protocol ChatListController: ViewController { diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index f1bb36cc4ef..28b68d96937 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -69,6 +69,8 @@ public enum ContactMultiselectionControllerMode { case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool) case channelCreation case chatSelection(ChatSelection) + case premiumGifting + case requestedUsersSelection } public enum ContactListFilter { diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift index 9d815f99218..81aae9de5b6 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerFrameSource.swift @@ -592,9 +592,9 @@ public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource return nil } self.animation = animation - let frameCount = Int(animation.frameCount) + let frameCount = max(1, Int(animation.frameCount)) self.frameCount = frameCount - self.frameRate = Int(animation.frameRate) + self.frameRate = max(1, Int(animation.frameRate)) self.cache = cachePathPrefix.flatMap { cachePathPrefix in AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier, useHardware: useMetalCache) diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index b8289bd6d97..8031e33178a 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -261,6 +261,9 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke } } + + public var forceSynchronous = false + public init(useMetalCache: Bool = false) { self.queue = sharedQueue self.eventsNode = AnimatedStickerNodeDisplayEvents() @@ -366,7 +369,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke strongSelf.play(firstFrame: true) } } - self.disposable.set((source.directDataPath(attemptSynchronously: false) + self.disposable.set((source.directDataPath(attemptSynchronously: self.forceSynchronous) |> filter { $0 != nil } |> deliverOnMainQueue).startStrict(next: { path in f(path!) @@ -698,7 +701,8 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke let frameSourceHolder = self.frameSource let timerHolder = self.timer let useMetalCache = self.useMetalCache - self.queue.async { [weak self] in + + let action = { [weak self] in var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value if case .timestamp = position { } else { @@ -795,6 +799,11 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke frameQueue.generateFramesIfNeeded() } } + if self.forceSynchronous { + action() + } else { + self.queue.async(action) + } } public func playIfNeeded() -> Bool { diff --git a/submodules/AppBundle/Sources/AppBundle/AppBundle.m b/submodules/AppBundle/Sources/AppBundle/AppBundle.m index 66f5e08fc8d..e93a19a70d7 100644 --- a/submodules/AppBundle/Sources/AppBundle/AppBundle.m +++ b/submodules/AppBundle/Sources/AppBundle/AppBundle.m @@ -1,15 +1,21 @@ #import NSBundle * _Nonnull getAppBundle() { - NSBundle *bundle = [NSBundle mainBundle]; - if ([[bundle.bundleURL pathExtension] isEqualToString:@"appex"]) { - bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]]; - } else if ([[bundle.bundleURL pathExtension] isEqualToString:@"framework"]) { - bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]]; - } else if ([[bundle.bundleURL pathExtension] isEqualToString:@"Frameworks"]) { - bundle = [NSBundle bundleWithURL:[bundle.bundleURL URLByDeletingLastPathComponent]]; - } - return bundle; + static NSBundle *appBundle = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSBundle *bundle = [NSBundle mainBundle]; + if ([[bundle.bundleURL pathExtension] isEqualToString:@"appex"]) { + bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]]; + } else if ([[bundle.bundleURL pathExtension] isEqualToString:@"framework"]) { + bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]]; + } else if ([[bundle.bundleURL pathExtension] isEqualToString:@"Frameworks"]) { + bundle = [NSBundle bundleWithURL:[bundle.bundleURL URLByDeletingLastPathComponent]]; + } + appBundle = bundle; + }); + + return appBundle; } @implementation UIImage (AppBundle) diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index a65befe803f..30c75c6e2b0 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -507,7 +507,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS guard let presentationInterfaceState = self.presentationInterfaceState else { return 0.0 } - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) } public func setCaption(_ caption: NSAttributedString?) { @@ -634,14 +634,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics) var textFieldMinHeight: CGFloat = 35.0 + var textFieldRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, metrics: metrics) + textFieldRealInsets = calculateTextFieldRealInsets(presentationInterfaceState) } let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: 0.0) + + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textFieldRealInsets.right) let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 0fdf68b1829..6a38691286f 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -795,8 +795,6 @@ public final class AvatarNode: ASDisplayNode { } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - assertNotOnMainThread() - let context = UIGraphicsGetCurrentContext()! if !isRasterizing { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index bdc23e1a58b..0b323ae9fc7 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -903,10 +903,17 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz guard let publicToken = nativeParams["public_token"] as? String else { return } + + var customTokenizeUrl: String? + if let value = nativeParams["public_token"] as? String, let url = URL(string: value), let host = url.host { + if url.scheme == "https" && (host == "smart-glocal.com" || host.hasSuffix(".smart-glocal.com")) { + customTokenizeUrl = value + } + } var dismissImpl: (() -> Void)? let canSave = paymentForm.canSaveCredentials || paymentForm.passwordMissing - let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken), completion: { method in + let controller = BotCheckoutNativeCardEntryController(context: strongSelf.context, provider: .smartglobal(isTesting: paymentForm.invoice.isTest, publicToken: publicToken, customTokenizeUrl: customTokenizeUrl), completion: { method in guard let strongSelf = self else { return } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index ab4254bd9fe..74af7587111 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -30,7 +30,7 @@ struct BotCheckoutNativeCardEntryAdditionalFields: OptionSet { final class BotCheckoutNativeCardEntryController: ViewController { enum Provider { case stripe(additionalFields: BotCheckoutNativeCardEntryAdditionalFields, publishableKey: String) - case smartglobal(isTesting: Bool, publicToken: String) + case smartglobal(isTesting: Bool, publicToken: String, customTokenizeUrl: String?) } private var controllerNode: BotCheckoutNativeCardEntryControllerNode { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index 0602b878ef1..9f497c90b9f 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -310,9 +310,11 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, })) self.updateDone() - case let .smartglobal(isTesting, publicToken): + case let .smartglobal(isTesting, publicToken, customTokenizeUrl): let url: String - if isTesting { + if let customTokenizeUrl { + url = customTokenizeUrl + } else if isTesting { url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card" } else { url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card" diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index b30c2f8b33d..021189f1e65 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -255,6 +255,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController groupCallPanelSource = .all case let .forum(peerId): groupCallPanelSource = .peer(peerId) + case .savedMessagesChats: + groupCallPanelSource = .none } self.tabsNode = SparseNode() @@ -282,6 +284,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController case let .forum(peerId): title = "" self.forumChannelTracker = ForumChannelTopics(account: self.context.account, peerId: peerId) + case .savedMessagesChats: + title = "" } let primaryContext = ChatListLocationContext( @@ -368,6 +372,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ))) case .forum: break + case .savedMessagesChats: + break } let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -376,6 +382,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } case .forum: break + case .savedMessagesChats: + break } } @@ -788,12 +796,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.reloadFilters() } - // MARK: Nicegram - if #available(iOS 13.0, *) { - self.nicegramInit() - } - // - self.storiesPostingAvailabilityDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { view -> AppConfiguration in let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue @@ -870,6 +872,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } return true } + case .savedMessagesChats: + self.navigationBar?.userInfo = nil + self.navigationBar?.allowsCustomTransition = { + return false + } } } @@ -1397,7 +1404,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let threadId = threadId { let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( - messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1433,7 +1440,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let source: ContextContentSource let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .replyThread(message: ChatReplyThreadMessage( - messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + messageId: MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )), subject: nil, botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) @@ -1958,7 +1965,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let mediaId = info.media.id { validIds.append(mediaId) if self.preloadStoryResourceDisposables[mediaId] == nil { - self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: self.context, peer: info.peer, storyId: info.storyId, media: info.media, reactions: info.reactions).startStrict() + self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: self.context, info: info).startStrict() } } } @@ -4526,20 +4533,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController view.viewWithTag(AssistantButton.tag) as? AssistantButton } - @available(iOS 13.0, *) - private func nicegramInit() { - RepoTgHelper.setTelegramId(context.account.peerId.id._internalGetInt64Value()) - - // Localization - let _ = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]).start { sharedData in - if let localizationSettings = sharedData.entries[SharedDataKeys.localizationSettings]?.get(LocalizationSettings.self) { - let activeLanguageCode = localizationSettings.secondaryComponent?.languageCode ?? localizationSettings.primaryComponent.languageCode - ng_setTgLangCode(activeLanguageCode) - setPreferredTranslationTargetLanguage(code: activeLanguageCode) - } - } - } - public override var keyShortcuts: [KeyShortcut] { let strings = self.presentationData.strings @@ -4924,6 +4917,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) })) + case .savedMessagesChats: + break } } } @@ -6223,6 +6218,9 @@ private final class ChatListLocationContext { presentationData: presentationData ) }) + case .savedMessagesChats: + self.didSetReady = true + self.ready.set(.single(true)) } let context = self.context @@ -6259,6 +6257,8 @@ private final class ChatListLocationContext { |> map { options -> (ChatListSelectionOptions, Set, Set)? in return (options, selectedPeerIds, selectedThreadIds) } + case .savedMessagesChats: + return .single(nil) } } else { @@ -6385,6 +6385,8 @@ private final class ChatListLocationContext { } case .forum: defaultTitle = "" + case .savedMessagesChats: + defaultTitle = "" } let previousEditingAndNetworkState = self.previousEditingAndNetworkStateValue.swap((stateAndFilterId.state.editing, networkState)) @@ -6626,7 +6628,7 @@ private final class ChatListLocationContext { strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, - content: .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), + content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), tapped: { [weak self] in guard let self else { return @@ -6703,6 +6705,8 @@ private final class ChatListLocationContext { } case let .forum(peerId): ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: parentController, isViewingAsTopics: true, sourceView: sourceView, gesture: nil) + case .savedMessagesChats: + break } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index ef04a62b29e..ebcb16827f6 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -803,7 +803,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { chatThreadInfo = nil var displayAsMessage = false switch location { - case .chatList: + case .chatList, .savedMessagesChats: index = .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index)) case let .forum(peerId): let _ = peerId @@ -1690,7 +1690,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) let foundThreads: Signal<[EngineChatList.Item], NoError> - if case .forum = location, (key == .topics || key == .chats) { + if case let .forum(peerId) = location, (key == .topics || key == .chats) { foundThreads = chatListViewForLocation(chatListLocation: location, location: .initial(count: 1000, filter: nil), account: context.account) |> map { view -> [EngineChatList.Item] in var filteredItems: [EngineChatList.Item] = [] @@ -1708,6 +1708,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return filteredItems } + |> mapToSignal { local -> Signal<[EngineChatList.Item], NoError> in + return .single(local) + |> then(context.engine.messages.searchForumTopics(peerId: peerId, query: finalQuery) + |> map { remoteResult in + var mergedResult = local + for item in remoteResult { + guard case let .forum(threadId) = item.id else { + continue + } + if !mergedResult.contains(where: { $0.id == .forum(threadId) }) { + mergedResult.append(item) + } + } + + return mergedResult + }) + } + |> distinctUntilChanged } else { foundThreads = .single([]) } @@ -2218,6 +2236,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openPremiumGift: { }, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: { @@ -2232,6 +2251,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let sourceNode = sourceNode as? ChatListItemNode { self.interaction.openStories?(id, sourceNode.avatarNode) } + }, dismissNotice: { _ in }) chatListInteraction.isSearchMode = true @@ -3538,10 +3558,11 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 5db4d9194e1..d07e4174d98 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,8 @@ final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index be68b94b04b..71343fc6359 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -550,6 +550,8 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation } case .forum: return [] + case .savedMessagesChats: + return [] } } @@ -990,6 +992,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarTimerBadge: AvatarBadgeView? let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? + var verifiedIconView: ComponentHostView? + var verifiedIconComponent: EmojiStatusComponent? var credibilityIconView: ComponentHostView? var credibilityIconComponent: EmojiStatusComponent? let mutedIconNode: ASImageNode @@ -1173,6 +1177,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.textNode.visibilityRect = self.visibilityStatus ? CGRect.infinite : nil + if let verifiedIconView = self.verifiedIconView, let verifiedIconComponent = self.verifiedIconComponent { + let _ = verifiedIconView.update( + transition: .immediate, + component: AnyComponent(verifiedIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: verifiedIconView.bounds.size + ) + } if let credibilityIconView = self.credibilityIconView, let credibilityIconComponent = self.credibilityIconComponent { let _ = credibilityIconView.update( transition: .immediate, @@ -1768,6 +1780,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let useChatListLayout: Bool if case .chatList = item.chatListLocation { useChatListLayout = true + } else if case .savedMessagesChats = item.chatListLocation { + useChatListLayout = true } else if displayAsMessage { useChatListLayout = true } else { @@ -1798,6 +1812,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentPinnedIconImage: UIImage? var currentMutedIconImage: UIImage? var currentCredibilityIconContent: EmojiStatusComponent.Content? + var currentVerifiedIconContent: EmojiStatusComponent.Content? var currentSecretIconImage: UIImage? var currentForwardedIcon: UIImage? var currentStoryIcon: UIImage? @@ -1955,7 +1970,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } var peerText: String? - if case .groupReference = item.content { + if case .savedMessagesChats = item.chatListLocation { + if let message = messages.last, let forwardInfo = message.forwardInfo, let author = forwardInfo.author { + if author.id != itemPeer.chatMainPeer?.id { + peerText = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } + } else if case .groupReference = item.content { if let messagePeer = itemPeer.chatMainPeer { peerText = messagePeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } @@ -2180,7 +2201,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = composedString - if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) { + if case .savedMessagesChats = item.chatListLocation { + displayForwardedIcon = false + } else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) { displayForwardedIcon = true } else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) { displayStoryReplyIcon = true @@ -2529,7 +2552,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { + if case .channel = peer, peer.isVerified { + currentVerifiedIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + } + currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -2545,7 +2572,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { + if case .channel = peer, peer.isVerified { + currentVerifiedIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + } + currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -2566,7 +2597,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let currentSecretIconImage = currentSecretIconImage { titleIconsWidth += currentSecretIconImage.size.width + 2.0 } - if let currentCredibilityIconContent = currentCredibilityIconContent { + + if let currentVerifiedIconContent { + if titleIconsWidth.isZero { + titleIconsWidth += 4.0 + } else { + titleIconsWidth += 2.0 + } + switch currentVerifiedIconContent { + case let .text(_, string): + let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) + let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) + titleIconsWidth += floor(stringRect.width) + 11.0 + default: + titleIconsWidth += 8.0 + } + } + + if let currentCredibilityIconContent { if titleIconsWidth.isZero { titleIconsWidth += 4.0 } else { @@ -3628,7 +3676,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { lastLineRect = CGRect(origin: CGPoint(), size: titleLayout.size) } - if let currentCredibilityIconContent = currentCredibilityIconContent { + if let currentCredibilityIconContent { let credibilityIconView: ComponentHostView if let current = strongSelf.credibilityIconView { credibilityIconView = current @@ -3661,6 +3709,39 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { credibilityIconView.removeFromSuperview() } + if let currentVerifiedIconContent { + let verifiedIconView: ComponentHostView + if let current = strongSelf.verifiedIconView { + verifiedIconView = current + } else { + verifiedIconView = ComponentHostView() + strongSelf.verifiedIconView = verifiedIconView + strongSelf.mainContentContainerNode.view.addSubview(verifiedIconView) + } + + let verifiedIconComponent = EmojiStatusComponent( + context: item.context, + animationCache: item.interaction.animationCache, + animationRenderer: item.interaction.animationRenderer, + content: currentVerifiedIconContent, + isVisibleForAnimations: strongSelf.visibilityStatus && item.context.sharedContext.energyUsageSettings.loopEmoji, + action: nil + ) + strongSelf.verifiedIconComponent = verifiedIconComponent + + let iconSize = verifiedIconView.update( + transition: .immediate, + component: AnyComponent(verifiedIconComponent), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize)) + nextTitleIconOrigin += verifiedIconView.bounds.width + 4.0 + } else if let verifiedIconView = strongSelf.verifiedIconView { + strongSelf.verifiedIconView = nil + verifiedIconView.removeFromSuperview() + } + if let currentMutedIconImage = currentMutedIconImage { strongSelf.mutedIconNode.image = currentMutedIconImage strongSelf.mutedIconNode.isHidden = false diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 4778982f7b3..20df0407547 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -311,6 +311,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } else { messageText = strings.Message_GiveawayStarted } + case let results as TelegramMediaGiveawayResults: + if results.winnersCount == 0 { + messageText = strings.Message_GiveawayEndedNoWinners + } else { + messageText = strings.Message_GiveawayEndedWinners(results.winnersCount) + } case let webpage as TelegramMediaWebpage: if messageText.isEmpty, case let .Loaded(content) = webpage.content { messageText = content.displayUrl diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 44543b84485..0d0549256f1 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -114,11 +114,13 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void + let openPremiumGift: () -> Void let openActiveSessions: () -> Void let performActiveSessionAction: (NewSessionReview, Bool) -> Void let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void + let dismissNotice: (ChatListNotice) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -167,11 +169,13 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, + openPremiumGift: @escaping () -> Void, openActiveSessions: @escaping () -> Void, performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, - openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void + openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, + dismissNotice: @escaping (ChatListNotice) -> Void ) { self.activateSearch = activateSearch // MARK: Nicegram PinnedChats @@ -207,11 +211,13 @@ public final class ChatListNodeInteraction { self.openStorageManagement = openStorageManagement self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro + self.openPremiumGift = openPremiumGift self.openActiveSessions = openActiveSessions self.performActiveSessionAction = performActiveSessionAction self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories + self.dismissNotice = dismissNotice } } @@ -721,7 +727,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { @@ -731,14 +737,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() + case .xmasPremiumGift: + nodeInteraction?.openPremiumGift() case .reviewLogin: break } case .hide: - switch notice { - default: - break - } + nodeInteraction?.dismissNotice(notice) case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview, _): @@ -1044,7 +1049,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { @@ -1054,14 +1059,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPasswordSetup() case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() + case .xmasPremiumGift: + nodeInteraction?.openPremiumGift() case .reviewLogin: break } case .hide: - switch notice { - default: - break - } + nodeInteraction?.dismissNotice(notice) case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview, _): @@ -1596,8 +1600,7 @@ public final class ChatListNode: ListView { } }, present: { [weak self] c in self?.present?(c) - }, - openForumThread: { [weak self] peerId, threadId in + }, openForumThread: { [weak self] peerId, threadId in guard let self else { return } @@ -1638,6 +1641,12 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) + }, openPremiumGift: { [weak self] in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context) + self.push?(controller) }, openActiveSessions: { [weak self] in guard let self else { return @@ -1724,6 +1733,20 @@ public final class ChatListNode: ListView { return } self.openStories?(subject, itemNode) + }, dismissNotice: { [weak self] notice in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + switch notice { + case .xmasPremiumGift: + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .xmasPremiumGift).startStandalone() + self.present?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.ChatList_PremiumGiftInSettingsInfo, timeout: 5.0, customUndoText: nil), elevatedLayout: false, action: { _ in + return true + })) + default: + break + } }) nodeInteraction.isInlineMode = isInlineMode @@ -1827,7 +1850,9 @@ public final class ChatListNode: ListView { return .single(.setupPassword) } } - if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { + if suggestions.contains(.xmasPremiumGift) { + return .single(.xmasPremiumGift) + } else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { return inAppPurchaseManager.availableProducts |> map { products -> ChatListNotice? in if products.count > 1 { @@ -2080,14 +2105,14 @@ public final class ChatListNode: ListView { suggestedChatListNotice.get(), savedMessagesPeer, chatListViewUpdate, - self.statePromise.get(), - contacts, // MARK: Nicegram PinnedChats - nicegramItemsPromise.get() + nicegramItemsPromise.get(), // + self.statePromise.get(), + contacts ) // MARK: Nicegram PinnedChats, nicegramItems added - |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts, nicegramItems) -> Signal in + |> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, nicegramItems, state, contacts) -> Signal in let (update, filter) = updateAndFilter let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) @@ -2682,6 +2707,8 @@ public final class ChatListNode: ListView { continue } threadId = threadIdValue + case .savedMessagesChats: + return } var cachedChatResult: [(EnginePeer, PeerInputActivity)] = [] for (peerId, activity) in activities { @@ -2732,6 +2759,8 @@ public final class ChatListNode: ListView { continue } itemId = ChatListNodePeerInputActivities.ItemId(peerId: chatPeerId.peerId, threadId: threadIdValue) + case .savedMessagesChats: + return [:] } var chatResult: [(EnginePeer, PeerInputActivity)] = [] @@ -2958,6 +2987,8 @@ public final class ChatListNode: ListView { } else { return .single(false) } + case .savedMessagesChats: + return .single(false) } } var startedScrollingWithCanExpandHiddenItems = false @@ -3271,8 +3302,17 @@ public final class ChatListNode: ListView { if case .chatList = strongSelf.mode { let entryCount = transition.chatListView.filteredEntries.count if entryCount >= 1 { - if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1].sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex != nil { - pinnedOverscroll = true + for i in 0 ..< 2 { + if entryCount - 1 - i < 0 { + continue + } + if case .PeerEntry = transition.chatListView.filteredEntries[entryCount - 1 - i] { + } else { + continue + } + if case let .index(index) = transition.chatListView.filteredEntries[entryCount - 1 - i].sortIndex, case let .chatList(chatListIndex) = index, chatListIndex.pinningIndex != nil { + pinnedOverscroll = true + } } } } @@ -3864,6 +3904,8 @@ public final class ChatListNode: ListView { self.threadSelectionPanState = (selecting, threadId, []) self.interaction?.toggleThreadsSelection([threadId], selecting) } + case .savedMessagesChats: + break } case .changed: self.handlePanSelection(location: location) @@ -3970,6 +4012,8 @@ public final class ChatListNode: ListView { } } } + case .savedMessagesChats: + break } guard hasState else { return diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 7e45a03628b..fd0c2d3cc6c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -85,12 +85,13 @@ public enum ChatListNodeEntryPromoInfo: Equatable { case psa(type: String, message: String?) } -enum ChatListNotice: Equatable { +public enum ChatListNotice: Equatable { case clearStorage(sizeFraction: Double) case setupPassword case premiumUpgrade(discount: Int32) case premiumAnnualDiscount(discount: Int32) case premiumRestore(discount: Int32) + case xmasPremiumGift case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index c325a20eb3d..18353e76f10 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -302,6 +302,102 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat isLoading: view.isLoading ) + let type: ViewUpdateType + if isFirst { + type = .Initial + } else { + type = .Generic + } + isFirst = false + return ChatListNodeViewUpdate(list: list, type: type, scrollPosition: nil) + } + case .savedMessagesChats: + var isFirst = true + + return account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 1000, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tagMask: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal])), orderStatistics: []) + |> map { view, _, _ -> ChatListNodeViewUpdate in + let isLoading = view.isLoading + + var items: [EngineChatList.Item] = [] + + var topMessageByPeerId: [EnginePeer.Id: Message] = [:] + if !isLoading { + for entry in view.entries { + guard let threadId = entry.message.threadId else { + continue + } + let sourcePeerId = PeerId(threadId) + + if let currentTopMessage = topMessageByPeerId[sourcePeerId] { + if currentTopMessage.index < entry.index { + topMessageByPeerId[sourcePeerId] = entry.message + } + } else { + topMessageByPeerId[sourcePeerId] = entry.message + } + } + for (_, message) in topMessageByPeerId.sorted(by: { $0.value.index > $1.value.index }) { + guard let threadId = message.threadId else { + continue + } + let sourceId = PeerId(threadId) + var sourcePeer = message.peers[sourceId] + if sourcePeer == nil, let forwardInfo = message.forwardInfo, let authorSignature = forwardInfo.authorSignature { + sourcePeer = TelegramUser( + id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(1)), + accessHash: nil, + firstName: authorSignature, + lastName: nil, + username: nil, + phone: nil, + photo: [], + botInfo: nil, + restrictionInfo: nil, + flags: [], + emojiStatus: nil, + usernames: [], + storiesHidden: nil, + nameColor: nil, + backgroundEmojiId: nil, + profileColor: nil, + profileBackgroundEmojiId: nil + ) + } + guard let sourcePeer else { + continue + } + let mappedMessageIndex = MessageIndex(id: MessageId(peerId: sourceId, namespace: message.index.id.namespace, id: message.index.id.id), timestamp: message.index.timestamp) + items.append(EngineChatList.Item( + id: .chatList(sourceId), + index: .chatList(ChatListIndex(pinningIndex: nil, messageIndex: mappedMessageIndex)), + messages: [EngineMessage(message)], + readCounters: nil, + isMuted: false, + draft: nil, + threadData: nil, + renderedPeer: EngineRenderedPeer(peer: EnginePeer(sourcePeer)), + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + forumTopicData: nil, + topForumTopicItems: [], + hasFailed: false, + isContact: false, + autoremoveTimeout: nil, + storyStats: nil + )) + } + } + + let list = EngineChatList( + items: items.reversed(), + groupItems: [], + additionalItems: [], + hasEarlier: false, + hasLater: false, + isLoading: isLoading + ) + let type: ViewUpdateType if isFirst { type = .Initial diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 7b2947a44e7..5d4f82c0915 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -7,6 +7,9 @@ import TelegramPresentationData import ListSectionHeaderNode import AppBundle import ItemListUI +import Markdown +import AccountContext +import TelegramCore class ChatListStorageInfoItem: ListViewItem { enum Action { @@ -15,6 +18,7 @@ class ChatListStorageInfoItem: ListViewItem { case buttonChoice(isPositive: Bool) } + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let notice: ChatListNotice @@ -22,7 +26,8 @@ class ChatListStorageInfoItem: ListViewItem { let selectable: Bool = true - init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) { + self.context = context self.theme = theme self.strings = strings self.notice = notice @@ -85,6 +90,8 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { private let arrowNode: ASImageNode private let separatorNode: ASDisplayNode + private var closeButton: HighlightableButtonNode? + private var okButtonText: TextNode? private var cancelButtonText: TextNode? private var okButton: HighlightableButtonNode? @@ -126,6 +133,13 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { super.didLoad() } + @objc private func closePressed() { + guard let item = self.item else { + return + } + item.action(.hide) + } + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() let (_, apply) = layout(item as! ChatListStorageInfoItem, params, nextItem == nil) @@ -203,6 +217,9 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { titleString = titleStringValue textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case .xmasPremiumGift: + titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil })) + textString = NSAttributedString(string: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case let .reviewLogin(newSessionReview, totalCount): spacing = 2.0 alignment = .center @@ -259,7 +276,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { if let image = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } - + if let okButtonLayout, let cancelButtonLayout { strongSelf.arrowNode.isHidden = true @@ -330,6 +347,31 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { } } + let arrowIsHidden = strongSelf.arrowNode.isHidden + if case .xmasPremiumGift = item.notice { + strongSelf.arrowNode.isHidden = true + + let closeButton: HighlightableButtonNode + if let current = strongSelf.closeButton { + closeButton = current + } else { + closeButton = HighlightableButtonNode() + closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside]) + strongSelf.contentContainer.addSubnode(closeButton) + strongSelf.closeButton = closeButton + } + + if themeUpdated { + closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal) + } + + let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0)) + closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize) + } else { + strongSelf.arrowNode.isHidden = arrowIsHidden + } + strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets diff --git a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift index 611a6966d56..c1f55412ec8 100644 --- a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift +++ b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift @@ -51,8 +51,8 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { private var iconName: String? private let backgroundNode: ASImageNode - private let durationNode: ASTextNode - private var sizeNode: ASTextNode? + public let durationNode: ASTextNode + public private(set) var sizeNode: ASTextNode? private var measureNode: ASTextNode private var iconNode: ASImageNode? private var mediaDownloadStatusNode: RadialStatusNode? diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index b39e98822ed..a37e42eff81 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -418,6 +418,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let translationState: ChatPresentationTranslationState? public let replyMessage: Message? public let accountPeerColor: AccountPeerColor? + public let savedMessagesTopicPeer: EnginePeer? public init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, threadData: ThreadData?, isGeneralThreadClosed: Bool?, replyMessage: Message?, accountPeerColor: AccountPeerColor?) { self.interfaceState = ChatInterfaceState() @@ -492,9 +493,10 @@ public final class ChatPresentationInterfaceState: Equatable { self.translationState = nil self.replyMessage = replyMessage self.accountPeerColor = accountPeerColor + self.savedMessagesTopicPeer = nil } - public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?) { + public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, savedMessagesTopicPeer: EnginePeer?) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -567,6 +569,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.translationState = translationState self.replyMessage = replyMessage self.accountPeerColor = accountPeerColor + self.savedMessagesTopicPeer = savedMessagesTopicPeer } public static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -792,35 +795,38 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.accountPeerColor != rhs.accountPeerColor { return false } + if lhs.savedMessagesTopicPeer != rhs.savedMessagesTopicPeer { + return false + } return true } public func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedIsNotAccessible(_ isNotAccessible: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedExplicitelyCanPinMessages(_ explicitelyCanPinMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedContactStatus(_ contactStatus: ChatContactStatus?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedHasBots(_ hasBots: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedIsArchived(_ isArchived: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -832,231 +838,235 @@ public final class ChatPresentationInterfaceState: Equatable { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedRecordedMediaPreview(_ recordedMediaPreview: ChatRecordedMediaPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPeerDiscussionId(_ peerDiscussionId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedCallsAvailable(_ callsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedCallsPrivate(_ callsPrivate: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedSlowmodeState(_ slowmodeState: ChatSlowmodeState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPresentationReady(_ presentationReady: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedTheme(_ theme: PresentationTheme) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedStrings(_ strings: PresentationStrings) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedDateTimeFormat(_ dateTimeFormat: PresentationDateTimeFormat) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedChatWallpaper(_ chatWallpaper: TelegramWallpaper) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedBubbleCorners(_ bubbleCorners: PresentationChatBubbleCorners) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPendingUnpinnedAllMessages(_ pendingUnpinnedAllMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedActiveGroupCallInfo(_ activeGroupCallInfo: ChatActiveGroupCallInfo?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedHasActiveGroupCall(_ hasActiveGroupCall: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedImportState(_ importState: ChatPresentationImportState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedReportReason(_ reportReason: ReportReason?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedShowCommands(_ showCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedHasBotCommands(_ hasBotCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedShowSendAsPeers(_ showSendAsPeers: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedSendAsPeers(_ sendAsPeers: [SendAsPeer]?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedCurrentSendAsPeerId(_ currentSendAsPeerId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedBotMenuButton(_ botMenuButton: BotMenuButton) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedShowWebView(_ showWebView: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedCopyProtectionEnabled(_ copyProtectionEnabled: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedHasPlentyOfMessages(_ hasPlentyOfMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedIsPremium(_ isPremium: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedSuggestPremiumGift(_ suggestPremiumGift: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedForceInputCommandsHidden(_ forceInputCommandsHidden: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedCustomEmojiAvailable(_ customEmojiAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedThreadData(_ threadData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedForumTopicData(_ forumTopicData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedIsGeneralThreadClosed(_ isGeneralThreadClosed: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedTranslationState(_ translationState: ChatPresentationTranslationState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedReplyMessage(_ replyMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) } public func updatedAccountPeerColor(_ accountPeerColor: AccountPeerColor?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer) + } + + public func updatedSavedMessagesTopicPeer(_ savedMessagesTopicPeer: EnginePeer?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer) } } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index cd1e713f21f..099ecbeadad 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -23,6 +23,7 @@ public final class ChatSendMessageActionSheetController: ViewController { } // MARK: Nicegram TranslateEnteredMessage + private let canTranslate: Bool private let translate: () -> Void private let chooseLanguage: () -> Void // @@ -54,8 +55,8 @@ public final class ChatSendMessageActionSheetController: ViewController { public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - // MARK: Nicegram TranslateEnteredMessage, change (translate + chooseLanguage) - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, translate: @escaping () -> Void = {}, chooseLanguage: @escaping () -> Void = {}, schedule: @escaping () -> Void) { + // MARK: Nicegram TranslateEnteredMessage, add (canTranslate, translate, chooseLanguage) + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, canTranslate: Bool = false, translate: @escaping () -> Void = {}, chooseLanguage: @escaping () -> Void = {}, schedule: @escaping () -> Void) { self.context = context self.peerId = peerId self.isScheduledMessages = isScheduledMessages @@ -69,6 +70,7 @@ public final class ChatSendMessageActionSheetController: ViewController { self.completion = completion self.sendMessage = sendMessage // MARK: Nicegram TranslateEnteredMessage + self.canTranslate = canTranslate self.translate = translate self.chooseLanguage = chooseLanguage // @@ -121,9 +123,6 @@ public final class ChatSendMessageActionSheetController: ViewController { } // MARK: Nicegram TranslateEnteredMessage - let isSecretChat = (peerId?.namespace == Namespaces.Peer.SecretChat) - let canTranslate = !isSecretChat - let interlocutorLangCode = getCachedLanguageCode(forChatWith: peerId) // // MARK: Nicegram TranslateEnteredMessage, change (interlocutorLangCode + translate + chooseLanguage) @@ -139,7 +138,7 @@ public final class ChatSendMessageActionSheetController: ViewController { }, schedule: !canSchedule ? nil : { [weak self] in self?.schedule() self?.dismiss(cancel: false) - }, canTranslate: canTranslate, interlocutorLangCode: interlocutorLangCode, translate: { [weak self] in + }, canTranslate: self.canTranslate, interlocutorLangCode: interlocutorLangCode, translate: { [weak self] in self?.translate() self?.dismiss(cancel: false) }, chooseLanguage: { [weak self] in diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 9b1887fbeed..57e319ce9cd 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -49,6 +49,8 @@ private extension Transition.Animation.Curve { switch self { case .easeInOut: return CAMediaTimingFunction(name: .easeInEaseOut) + case .linear: + return CAMediaTimingFunction(name: .linear) case let .custom(a, b, c, d): return CAMediaTimingFunction(controlPoints: a, b, c, d) case .spring: @@ -72,6 +74,7 @@ public struct Transition { public enum Curve { case easeInOut case spring + case linear case custom(Float, Float, Float, Float) public func solve(at offset: CGFloat) -> CGFloat { @@ -80,6 +83,8 @@ public struct Transition { return listViewAnimationCurveEaseInOut(offset) case .spring: return listViewAnimationCurveSystem(offset) + case .linear: + return offset case let .custom(c1x, c1y, c2x, c2y): return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset) } diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 3f51ef98bfc..32e0c7edfe2 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -11,16 +11,18 @@ public final class RoundedRectangle: Component { public let cornerRadius: CGFloat public let gradientDirection: GradientDirection public let stroke: CGFloat? + public let strokeColor: UIColor? - public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil) { - self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke) + public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor) } - public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil) { + public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection self.stroke = stroke + self.strokeColor = strokeColor } public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool { @@ -36,6 +38,9 @@ public final class RoundedRectangle: Component { if lhs.stroke != rhs.stroke { return false } + if lhs.strokeColor != rhs.strokeColor { + return false + } return true } @@ -48,11 +53,19 @@ public final class RoundedRectangle: Component { let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { - context.setFillColor(color.cgColor) + if let strokeColor = component.strokeColor { + context.setFillColor(strokeColor.cgColor) + } else { + context.setFillColor(color.cgColor) + } context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize)) if let stroke = component.stroke, stroke > 0.0 { - context.setBlendMode(.clear) + if let _ = component.strokeColor { + context.setFillColor(color.cgColor) + } else { + context.setBlendMode(.clear) + } context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke)) } } diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift index 2c50f910830..33adade5abd 100644 --- a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -7,7 +7,7 @@ public extension Transition.Animation.Curve { init(_ curve: ContainedViewLayoutTransitionCurve) { switch curve { case .linear: - self = .easeInOut + self = .linear case .easeInOut: self = .easeInOut case let .custom(a, b, c, d): @@ -21,6 +21,8 @@ public extension Transition.Animation.Curve { var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { switch self { + case .linear: + return .linear case .easeInOut: return .easeInOut case .spring: diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 6c1e3327661..1b70a3a8b6a 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -609,7 +609,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent currentCredibilityIcon = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { currentCredibilityIcon = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = item.peer.emojiStatus { currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(white: 0.0, alpha: 0.1), themeColor: presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { currentCredibilityIcon = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index d24d1e2a7ca..aedc76242ed 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -225,12 +225,25 @@ open class ViewControllerComponentContainer: ViewController { public var wasDismissed: (() -> Void)? - public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, presentationMode: PresentationMode = .default, theme: Theme = .default) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + public init( + context: AccountContext, + component: C, + navigationBarAppearance: NavigationBarAppearance, + statusBarStyle: StatusBarStyle = .default, + presentationMode: PresentationMode = .default, + theme: Theme = .default, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil + ) where C.EnvironmentType == ViewControllerComponentContainer.Environment { self.context = context self.component = AnyComponent(component) self.theme = theme - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData: PresentationData + if let updatedPresentationData { + presentationData = updatedPresentationData.initial + } else { + presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + } let navigationBarPresentationData: NavigationBarPresentationData? switch navigationBarAppearance { @@ -243,7 +256,7 @@ open class ViewControllerComponentContainer: ViewController { } super.init(navigationBarPresentationData: navigationBarPresentationData) - self.presentationDataDisposable = (self.context.sharedContext.presentationData + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? self.context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { var theme = presentationData.theme @@ -266,6 +279,19 @@ open class ViewControllerComponentContainer: ViewController { strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style } + let navigationBarPresentationData: NavigationBarPresentationData? + switch navigationBarAppearance { + case .none: + navigationBarPresentationData = nil + case .transparent: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true) + case .default: + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData) + } + if let navigationBarPresentationData { + strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData) + } + if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .immediate) } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index d1cbb233b5d..ed1c0318e34 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -59,16 +59,18 @@ private final class ContactListNodeInteraction { fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void + fileprivate let deselectAll: () -> Void let itemHighlighting = ContactItemHighlighting() - init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void) { + init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void) { self.activateSearch = activateSearch self.authorize = authorize self.suppressWarning = suppressWarning self.openPeer = openPeer self.contextAction = contextAction self.openStories = openStories + self.deselectAll = deselectAll } } @@ -358,7 +360,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } -private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer], interaction: ContactListNodeInteraction) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var commonHeader: ListViewItemHeader? @@ -421,7 +423,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis for i in 0 ..< options.count { entries.append(.option(i, options[i], commonHeader, theme, strings)) } - case let .natural(options, _): + case let .natural(options, _, _): let sortedPeers = peers.sorted(by: { lhs, rhs in let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder) if result == .orderedSame { @@ -513,6 +515,31 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } } + var existingPeerIds = Set() + if !topPeers.isEmpty { + let hasDeselectAll = !(selectionState?.selectedPeerIndices ?? [:]).isEmpty + + let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_FrequentContacts.uppercased(), AnyHashable(hasDeselectAll ? 1 : 0)), theme: theme, strings: strings, actionTitle: hasDeselectAll ? strings.Premium_Gift_ContactSelection_DeselectAll.uppercased() : nil, action: { + interaction.deselectAll() + }) + + var index: Int = 0 + for peer in topPeers.prefix(15) { + existingPeerIds.insert(.peer(peer.id)) + + let selection: ContactsPeerItemSelection + if let selectionState = selectionState { + selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil) + } else { + selection = .none + } + + let presence = presences[peer.id] + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil)) + + index += 1 + } + } if let storySubscriptions { let _ = storySubscriptions @@ -528,7 +555,6 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis var index: Int = 0 - var existingPeerIds = Set() if let selectionState = selectionState { for peer in selectionState.foundPeers { if existingPeerIds.contains(peer.id) { @@ -657,7 +683,7 @@ private struct ContactsListNodeTransition { public enum ContactListPresentation { case orderedByPresence(options: [ContactListAdditionalOption]) - case natural(options: [ContactListAdditionalOption], includeChatList: Bool) + case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool) case search(signal: Signal, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool) public var sortOrder: ContactsSortOrder? { @@ -806,6 +832,7 @@ public final class ContactListNode: ASDisplayNode { public var activateSearch: (() -> Void)? public var openPeer: ((ContactListPeer, ContactListAction) -> Void)? + public var deselectedAll: (() -> Void)? public var openPrivacyPolicy: (() -> Void)? public var suppressPermissionWarning: (() -> Void)? private let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? @@ -926,6 +953,14 @@ public final class ContactListNode: ASDisplayNode { return } self.openStories?(peer, sourceNode) + }, deselectAll: { [weak self] in + guard let self else { + return + } + self.updateSelectionState({ state in + return ContactListNodeGroupSelectionState() + }) + self.deselectedAll?() }) self.indexNode.indexSelected = { [weak self] section in @@ -983,9 +1018,11 @@ public final class ContactListNode: ASDisplayNode { |> mapToSignal { presentation in var generateSections = false var includeChatList = false - if case let .natural(_, includeChatListValue) = presentation { + var displayTopPeers = false + if case let .natural(_, includeChatListValue, displayTopPeersValue) = presentation { generateSections = true includeChatList = includeChatListValue + displayTopPeers = displayTopPeersValue } if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation { @@ -1214,7 +1251,7 @@ public final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact.0)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: [], interaction: interaction) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } @@ -1274,8 +1311,24 @@ public final class ContactListNode: ASDisplayNode { chatListSignal = .single([]) } - return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get()) - |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal in + let recentPeers: Signal + if displayTopPeers { + recentPeers = context.engine.peers.recentPeers() + } else { + recentPeers = .single(.disabled) + } + + return (combineLatest( + self.contactPeersViewPromise.get(), + chatListSignal, + selectionStateSignal, + presentationDataPromise.get(), + contactsAuthorization.get(), + contactsWarningSuppressed.get(), + self.storySubscriptions.get(), + recentPeers + ) + |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions, recentPeers -> Signal in let signal = deferred { () -> Signal in var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { @@ -1312,11 +1365,16 @@ public final class ContactListNode: ASDisplayNode { } } + var topPeers: [EnginePeer] = [] + if case let .peers(peers) = recentPeers { + topPeers = peers.map(EnginePeer.init) + } + var isEmpty = false if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } - let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions) + let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers, interaction: interaction) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index ade1b4a403e..5be7b54ceca 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -107,7 +107,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { case .presence: return .orderedByPresence(options: options) case .natural: - return .natural(options: options, includeChatList: false) + return .natural(options: options, includeChatList: false, topPeers: false) } } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 54165ebd7dc..8045723169e 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -404,6 +404,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let titleNode: TextNode private var credibilityIconView: ComponentHostView? private var credibilityIconComponent: EmojiStatusComponent? + private var verifiedIconView: ComponentHostView? + private var verifiedIconComponent: EmojiStatusComponent? private let statusNode: TextNode private var statusIconNode: ASImageNode? private var badgeBackgroundNode: ASImageNode? @@ -464,6 +466,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: credibilityIconView.bounds.size ) } + if let verifiedIconView = self.verifiedIconView, let verifiedIconComponent = self.verifiedIconComponent { + let _ = verifiedIconView.update( + transition: .immediate, + component: AnyComponent(verifiedIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: verifiedIconView.bounds.size + ) + } if let avatarIconView = self.avatarIconView, let avatarIconComponent = self.avatarIconComponent { let _ = avatarIconView.update( transition: .immediate, @@ -692,6 +702,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) var credibilityIcon: EmojiStatusComponent.Content? + var verifiedIcon: EmojiStatusComponent.Content? switch item.peer { case let .peer(peer, _): if let peer = peer, peer.id != item.context.account.peerId { @@ -699,7 +710,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = peer.emojiStatus { + if case .channel = peer, peer.isVerified { + verifiedIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + } credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -879,7 +893,18 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } var additionalTitleInset: CGFloat = 0.0 - if let credibilityIcon = credibilityIcon { + if let verifiedIcon { + additionalTitleInset += 3.0 + switch verifiedIcon { + case let .text(_, string): + let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) + let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) + additionalTitleInset += floor(stringRect.width) + 11.0 + default: + additionalTitleInset += 16.0 + } + } + if let credibilityIcon { additionalTitleInset += 3.0 switch credibilityIcon { case let .text(_, string): @@ -1193,7 +1218,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - if let credibilityIcon = credibilityIcon { + var nextIconX: CGFloat = titleFrame.maxX + if let credibilityIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1224,12 +1250,53 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: CGSize(width: 20.0, height: 20.0) ) - transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += 4.0 + transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += iconSize.width } else if let credibilityIconView = strongSelf.credibilityIconView { strongSelf.credibilityIconView = nil credibilityIconView.removeFromSuperview() } + if let verifiedIcon { + let animationCache = item.context.animationCache + let animationRenderer = item.context.animationRenderer + + let verifiedIconView: ComponentHostView + if let current = strongSelf.verifiedIconView { + verifiedIconView = current + } else { + verifiedIconView = ComponentHostView() + strongSelf.offsetContainerNode.view.addSubview(verifiedIconView) + strongSelf.verifiedIconView = verifiedIconView + } + + let verifiedIconComponent = EmojiStatusComponent( + context: item.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: verifiedIcon, + isVisibleForAnimations: strongSelf.visibilityStatus, + action: nil, + emojiFileUpdated: nil + ) + strongSelf.verifiedIconComponent = verifiedIconComponent + + let iconSize = verifiedIconView.update( + transition: .immediate, + component: AnyComponent(verifiedIconComponent), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + nextIconX += 4.0 + transition.updateFrame(view: verifiedIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += iconSize.width + } else if let verifiedIconView = strongSelf.verifiedIconView { + strongSelf.verifiedIconView = nil + verifiedIconView.removeFromSuperview() + } + if let actionButtons = actionButtons { if strongSelf.actionButtonNodes == nil { var actionButtonNodes: [HighlightableButtonNode] = [] @@ -1430,11 +1497,19 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { self.statusNode.frame = statusFrame transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) + var nextIconX = titleFrame.maxX if let credibilityIconView = self.credibilityIconView { var iconFrame = credibilityIconView.frame - iconFrame.origin.x = titleFrame.maxX + 4.0 + iconFrame.origin.x = nextIconX + 4.0 + nextIconX += 4.0 + iconFrame.width transition.updateFrame(view: credibilityIconView, frame: iconFrame) } + if let verifiedIconView = self.verifiedIconView { + var iconFrame = verifiedIconView.frame + iconFrame.origin.x = nextIconX + 4.0 + nextIconX += 4.0 + iconFrame.width + transition.updateFrame(view: verifiedIconView, frame: iconFrame) + } if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { var badgeBackgroundFrame = badgeBackgroundNode.frame diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 13679a82fd2..c5e577943ef 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1776,7 +1776,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega } contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentUnscaledSize = preferredSize } } else { @@ -1786,7 +1786,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) - if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentUnscaledSize = preferredSize } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index afef8cb144e..ada6f68ffeb 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -462,6 +462,11 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin } else { minSize.width += sideInset } + if self.item.additionalLeftIcon != nil { + minSize.width += 24.0 + minSize.width += iconSideInset + minSize.width += iconSpacing + } if let forcedHeight { minSize.height = forcedHeight } else { diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 53af3875c4a..4cade32e470 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -183,7 +183,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.controller.containerLayoutUpdated( ContainerViewLayout( size: size, - metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: parentLayout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), @@ -766,7 +766,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo defaultContentSize.height = min(defaultContentSize.height, 460.0) let contentSize: CGSize - if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { + if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentSize = preferredSize } else if let storedContentHeight = contentNode.storedContentHeight { contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight) diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index f8474bb3757..7ecc6f8c35f 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,10 +81,9 @@ private enum DebugControllerEntry: ItemListNodeEntry { case logToConsole(PresentationTheme, Bool) case redactSensitiveData(PresentationTheme, Bool) case keepChatNavigationStack(PresentationTheme, Bool) - case skipReadHistory(PresentationTheme, Bool) case unidirectionalSwipeToReply(Bool) - case dustEffect(Bool) - case callUIV2(Bool) + case callV2(Bool) + case alternativeStoryMedia(Bool) case crashOnSlowQueries(PresentationTheme, Bool) case crashOnMemoryPressure(PresentationTheme, Bool) case clearTips(PresentationTheme) @@ -141,7 +140,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .webViewInspection, .resetWebViewCache: return DebugControllerSection.web.rawValue - case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .dustEffect, .callUIV2, .crashOnSlowQueries, .crashOnMemoryPressure: + case .keepChatNavigationStack, .unidirectionalSwipeToReply, .callV2, .alternativeStoryMedia, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue @@ -196,13 +195,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 14 case .keepChatNavigationStack: return 15 - case .skipReadHistory: - return 16 case .unidirectionalSwipeToReply: return 17 - case .dustEffect: + case .callV2: return 18 - case .callUIV2: + case .alternativeStoryMedia: return 19 case .crashOnSlowQueries: return 20 @@ -990,14 +987,6 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .skipReadHistory(_, value): - return ItemListSwitchItem(presentationData: presentationData, title: "Skip read history", value: value, sectionId: self.section, style: .blocks, updated: { value in - let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in - var settings = settings - settings.skipReadHistory = value - return settings - }).start() - }) case let .unidirectionalSwipeToReply(value): return ItemListSwitchItem(presentationData: presentationData, title: "Legacy swipe to reply", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in @@ -1006,19 +995,19 @@ private enum DebugControllerEntry: ItemListNodeEntry { return settings }).start() }) - case let .dustEffect(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Dust Effect", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .callV2(value): + return ItemListSwitchItem(presentationData: presentationData, title: "CallV2", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings - settings.dustEffect = value + settings.callV2 = value return settings }).start() }) - case let .callUIV2(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Call UI V2", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .alternativeStoryMedia(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Story Data Saver", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in var settings = settings - settings.callUIV2 = value + settings.alternativeStoryMedia = value return settings }).start() }) @@ -1499,10 +1488,9 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetWebViewCache(presentationData.theme)) entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) - entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) entries.append(.unidirectionalSwipeToReply(experimentalSettings.unidirectionalSwipeToReply)) - entries.append(.dustEffect(experimentalSettings.dustEffect)) - entries.append(.callUIV2(experimentalSettings.callUIV2)) + entries.append(.callV2(experimentalSettings.callV2)) + entries.append(.alternativeStoryMedia(experimentalSettings.alternativeStoryMedia)) } entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries)) entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure)) diff --git a/submodules/Display/Source/ChildWindowHostView.swift b/submodules/Display/Source/ChildWindowHostView.swift index f4b7a998b9b..cda3f5178f7 100644 --- a/submodules/Display/Source/ChildWindowHostView.swift +++ b/submodules/Display/Source/ChildWindowHostView.swift @@ -69,13 +69,15 @@ public func childWindowHostView(parent: UIView) -> WindowHostView { let hostView = WindowHostView(containerView: view, eventView: view, isRotating: { return false - }, systemUserInterfaceStyle: .single(.light), updateSupportedInterfaceOrientations: { orientations in + }, systemUserInterfaceStyle: .single(.light), currentInterfaceOrientation: { + return .portrait + }, updateSupportedInterfaceOrientations: { orientations in }, updateDeferScreenEdgeGestures: { edges in }, updatePrefersOnScreenNavigationHidden: { value in }) view.updateSize = { [weak hostView] size in - hostView?.updateSize?(size, 0.0) + hostView?.updateSize?(size, 0.0, .portrait) } view.layoutSubviewsEvent = { [weak hostView] in diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index cbdc55b4a27..a5b8eefe162 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1573,6 +1573,56 @@ public extension ContainedViewLayoutTransition { } } + func updateLineWidth(layer: CAShapeLayer, lineWidth: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if layer.lineWidth == lineWidth { + completion?(true) + return + } + + switch self { + case .immediate: + layer.removeAnimation(forKey: "lineWidth") + layer.lineWidth = lineWidth + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let fromLineWidth = layer.lineWidth + layer.lineWidth = lineWidth + layer.animate(from: fromLineWidth as NSNumber, to: lineWidth as NSNumber, keyPath: "lineWidth", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + + func updateStrokeColor(layer: CAShapeLayer, strokeColor: UIColor, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + if layer.strokeColor.flatMap(UIColor.init(cgColor:)) == strokeColor { + completion?(true) + return + } + + switch self { + case .immediate: + layer.removeAnimation(forKey: "strokeColor") + layer.strokeColor = strokeColor.cgColor + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let fromStrokeColor = layer.strokeColor ?? UIColor.clear.cgColor + layer.strokeColor = strokeColor.cgColor + layer.animate(from: fromStrokeColor, to: strokeColor.cgColor, keyPath: "strokeColor", timingFunction: curve.timingFunction, duration: duration, delay: delay, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + func attachAnimation(view: UIView, id: String, completion: @escaping (Bool) -> Void) { switch self { case .immediate: diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index 09552bbdaeb..8325f282588 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -23,15 +23,18 @@ public enum ContainerViewLayoutSizeClass { public struct LayoutMetrics: Equatable { public let widthClass: ContainerViewLayoutSizeClass public let heightClass: ContainerViewLayoutSizeClass + public let orientation: UIInterfaceOrientation? - public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) { + public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass, orientation: UIInterfaceOrientation?) { self.widthClass = widthClass self.heightClass = heightClass + self.orientation = orientation } public init() { self.widthClass = .compact self.heightClass = .compact + self.orientation = nil } } diff --git a/submodules/Display/Source/ContextContentContainerNode.swift b/submodules/Display/Source/ContextContentContainerNode.swift index 1741434fa77..d2e1081db6e 100644 --- a/submodules/Display/Source/ContextContentContainerNode.swift +++ b/submodules/Display/Source/ContextContentContainerNode.swift @@ -22,7 +22,7 @@ public final class ContextContentContainerNode: ASDisplayNode { transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size)) transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width) controller.updateLayout(size: size, transition: transition) - controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) } } } diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index a8ab764dcda..4d5c4eee6ae 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -432,11 +432,12 @@ public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = tr }, opaque: opaque, scale: scale) } -public func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? { +public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloat = 0.0) -> UIImage? { return generateImage(size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - }) + }, scale: scale) } public enum DrawingContextBltMode { @@ -931,3 +932,68 @@ public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: } } } + +public func convertSvgPath(_ path: StaticString) throws -> CGPath { + var index: UnsafePointer = path.utf8Start + let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount) + var currentPoint = CGPoint() + + let result = CGMutablePath() + + while index < end { + let c = index.pointee + index = index.successor() + + if c == 77 { // M + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: x, y: y) + result.move(to: currentPoint) + } else if c == 76 { // L + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Line to \(x), \(y)") + currentPoint = CGPoint(x: x, y: y) + result.addLine(to: currentPoint) + } else if c == 72 { // H + let x = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: x, y: currentPoint.y) + result.addLine(to: currentPoint) + } else if c == 86 { // V + let y = try readCGFloat(&index, end: end, separator: 32) + + //print("Move to \(x), \(y)") + currentPoint = CGPoint(x: currentPoint.x, y: y) + result.addLine(to: currentPoint) + } else if c == 67 { // C + let x1 = try readCGFloat(&index, end: end, separator: 44) + let y1 = try readCGFloat(&index, end: end, separator: 32) + let x2 = try readCGFloat(&index, end: end, separator: 44) + let y2 = try readCGFloat(&index, end: end, separator: 32) + let x = try readCGFloat(&index, end: end, separator: 44) + let y = try readCGFloat(&index, end: end, separator: 32) + + currentPoint = CGPoint(x: x, y: y) + result.addCurve(to: currentPoint, control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2)) + } else if c == 90 { // Z + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + } else if c == 83 { // S + if index != end && index.pointee != 32 { + throw ParsingError.Generic + } + } else if c == 32 { // space + continue + } else { + throw ParsingError.Generic + } + } + + return result +} diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 73111b4f6d4..ce2f60b0d02 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -4739,7 +4739,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture if self.experimentalSnapScrollToItem { self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: ListViewScrollPosition.visible, animated: animated, curve: ListViewAnimationCurve.Default(duration: nil), directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } else { - if node.frame.minY < self.insets.top { + if node.frame.minY < self.insets.top + overflow { if !allowIntersection || node.frame.maxY < self.insets.top { let position: ListViewScrollPosition if allowIntersection { @@ -4749,7 +4749,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: ListViewDeleteAndInsertOptions(), scrollToItem: ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: ListViewScrollToItemDirectionHint.Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } - } else if node.frame.maxY > self.visibleSize.height - self.insets.bottom { + } else if node.frame.maxY > self.visibleSize.height - self.insets.bottom - overflow { if !allowIntersection || node.frame.minY > self.visibleSize.height - self.insets.bottom { let position: ListViewScrollPosition if allowIntersection { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index 7386ec3f179..bec5fbf86ca 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -52,17 +52,19 @@ public enum ListViewItemNodeVisibility: Equatable { case visible(CGFloat, CGRect) } -public struct ListViewItemLayoutParams { +public struct ListViewItemLayoutParams: Equatable { public let width: CGFloat public let leftInset: CGFloat public let rightInset: CGFloat public let availableHeight: CGFloat + public let isStandalone: Bool - public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat) { + public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, availableHeight: CGFloat, isStandalone: Bool = false) { self.width = width self.leftInset = leftInset self.rightInset = rightInset self.availableHeight = availableHeight + self.isStandalone = isStandalone } } diff --git a/submodules/Display/Source/NativeWindowHostView.swift b/submodules/Display/Source/NativeWindowHostView.swift index eaa9b245d14..06099efdcd8 100644 --- a/submodules/Display/Source/NativeWindowHostView.swift +++ b/submodules/Display/Source/NativeWindowHostView.swift @@ -12,6 +12,24 @@ private let defaultOrientations: UIInterfaceOrientationMask = { } }() +func getCurrentViewInterfaceOrientation(view: UIView) -> UIInterfaceOrientation { + var orientation: UIInterfaceOrientation = .portrait + if #available(iOS 13.0, *) { + if let window = view as? UIWindow { + if let windowScene = window.windowScene { + orientation = windowScene.interfaceOrientation + } + } else { + if let windowScene = view.window?.windowScene { + orientation = windowScene.interfaceOrientation + } + } + } else { + orientation = UIApplication.shared.statusBarOrientation + } + return orientation +} + public enum WindowUserInterfaceStyle { case light case dark @@ -74,7 +92,7 @@ private final class WindowRootViewController: UIViewController, UIViewController private var registeredForPreviewing = false var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)? - var transitionToSize: ((CGSize, Double) -> Void)? + var transitionToSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)? private var _systemUserInterfaceStyle = ValuePromise(ignoreRepeated: true) var systemUserInterfaceStyle: Signal { @@ -182,8 +200,10 @@ private final class WindowRootViewController: UIViewController, UIViewController override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) + + let orientation = getCurrentViewInterfaceOrientation(view: self.view) UIView.performWithoutAnimation { - self.transitionToSize?(size, coordinator.transitionDuration) + self.transitionToSize?(size, coordinator.transitionDuration, orientation) } } @@ -372,18 +392,29 @@ public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) { rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size) rootViewController.viewDidAppear(false) - let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: { - return window.isRotating() - }, systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, updateSupportedInterfaceOrientations: { orientations in - rootViewController.orientations = orientations - }, updateDeferScreenEdgeGestures: { edges in - rootViewController.gestureEdges = edges - }, updatePrefersOnScreenNavigationHidden: { value in - rootViewController.prefersOnScreenNavigationHidden = value - }) - - rootViewController.transitionToSize = { [weak hostView] size, duration in - hostView?.updateSize?(size, duration) + let hostView = WindowHostView( + containerView: rootViewController.view, + eventView: window, + isRotating: { + return window.isRotating() + }, + systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, + currentInterfaceOrientation: { + return getCurrentViewInterfaceOrientation(view: window) + }, + updateSupportedInterfaceOrientations: { orientations in + rootViewController.orientations = orientations + }, + updateDeferScreenEdgeGestures: { edges in + rootViewController.gestureEdges = edges + }, + updatePrefersOnScreenNavigationHidden: { value in + rootViewController.prefersOnScreenNavigationHidden = value + } + ) + + rootViewController.transitionToSize = { [weak hostView] size, duration, orientation in + hostView?.updateSize?(size, duration, orientation) } window.updateSize = { _ in diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index e49739fc004..c2783805c46 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -101,7 +101,9 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { public final var supportedOrientations: ViewControllerSupportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) { didSet { if self.supportedOrientations != oldValue { - self.window?.invalidateSupportedOrientations() + if self.isNodeLoaded { + self.window?.invalidateSupportedOrientations() + } } } } diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index adb2e7aabf6..cdc5ee9df87 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -156,6 +156,7 @@ public final class WindowHostView { public let eventView: UIView public let isRotating: () -> Bool public let systemUserInterfaceStyle: Signal + public let currentInterfaceOrientation: () -> UIInterfaceOrientation let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void @@ -166,7 +167,7 @@ public final class WindowHostView { var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)? var presentNative: ((UIViewController) -> Void)? var nativeController: (() -> UIViewController?)? - var updateSize: ((CGSize, Double) -> Void)? + var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)? var layoutSubviews: (() -> Void)? var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)? var isUpdatingOrientationLayout = false @@ -178,11 +179,12 @@ public final class WindowHostView { var forEachController: (((ContainableController) -> Void) -> Void)? var getAccessibilityElements: (() -> [Any]?)? - init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) { + init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal, currentInterfaceOrientation: @escaping () -> UIInterfaceOrientation, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) { self.containerView = containerView self.eventView = eventView self.isRotating = isRotating self.systemUserInterfaceStyle = systemUserInterfaceStyle + self.currentInterfaceOrientation = currentInterfaceOrientation self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden @@ -220,11 +222,11 @@ public extension UIView { } } -private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics { +private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOrientation?) -> LayoutMetrics { if size.width > 690.0 && size.height > 650.0 { - return LayoutMetrics(widthClass: .regular, heightClass: .regular) + return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation) } else { - return LayoutMetrics(widthClass: .compact, heightClass: .compact) + return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation) } } @@ -350,11 +352,13 @@ public class Window1 { self.keyboardViewManager = nil } - let isLandscape = boundsSize.width > boundsSize.height + let isLandscape = boundsSize.width > boundsSize.height let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape) let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) - self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) + let orientation: UIInterfaceOrientation = self.hostView.currentInterfaceOrientation() + + self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(size: boundsSize, orientation: orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning) self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate) self.presentationContext = PresentationContext() self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView) @@ -406,8 +410,8 @@ public class Window1 { self?.presentNative(controller) } - self.hostView.updateSize = { [weak self] size, duration in - self?.updateSize(size, duration: duration) + self.hostView.updateSize = { [weak self] size, duration, orientation in + self?.updateSize(size, duration: duration, orientation: orientation) } self.hostView.layoutSubviews = { [weak self] in @@ -807,14 +811,14 @@ public class Window1 { return self.viewController?.view.hitTest(point, with: event) } - func updateSize(_ value: CGSize, duration: Double) { + func updateSize(_ value: CGSize, duration: Double, orientation: UIInterfaceOrientation) { let transition: ContainedViewLayoutTransition if !duration.isZero { transition = .animated(duration: duration, curve: .easeInOut) } else { transition = .immediate } - self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } + self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(size: value, orientation: orientation), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) } if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground { self.layoutSubviews(force: true) } @@ -1119,7 +1123,7 @@ public class Window1 { } let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout) - self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) + self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(size: updatingLayout.layout.size, orientation: updatingLayout.layout.metrics.orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver) let childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics) let childLayoutUpdated = self.updatedContainerLayout != childLayout diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 988ae6f3fd9..108213a0bd6 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -105,6 +105,7 @@ swift_library( "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/Camera", + "//submodules/TelegramUI/Components/DustEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 04b99612282..78d220a9116 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -36,6 +36,7 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D private func prepareForRendering(entityView: DrawingEntityView) { if let entityView = entityView as? DrawingStickerEntityView { entityView.entity.renderImage = entityView.getRenderImage() + entityView.entity.renderSubEntities = entityView.getRenderSubEntities() } if let entityView = entityView as? DrawingBubbleEntityView { entityView.entity.renderImage = entityView.getRenderImage() @@ -770,6 +771,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { selectionView.handlePan(gestureRecognizer) } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .dualVideoReference = stickerEntity.content { selectionView.handlePan(gestureRecognizer) + } else if let stickerEntity = selectedEntityView.entity as? DrawingStickerEntity, case .message = stickerEntity.content { + selectionView.handlePan(gestureRecognizer) } else { var isTrappedInBin = false let scale = 100.0 / selectedEntityView.bounds.size.width @@ -835,12 +838,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { selectionView.handlePan(gestureRecognizer) } } - } - else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { + } else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { self.selectEntity(viewToSelect.entity, animate: false) self.onInteractionUpdated(true) - } - else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { + } else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { mediaEntityView.handlePan(gestureRecognizer) } } @@ -961,7 +962,7 @@ public class DrawingEntityView: UIView { } selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) - selectionView.layer.animateScale(from: 0.87, to: 1.0, duration: 0.2, delay: 0.1) + selectionView.layer.animateScale(from: 0.88, to: 1.0, duration: 0.23, delay: 0.1) let values = [self.entity.scale, self.entity.scale * 0.88, self.entity.scale] let keyTimes = [0.0, 0.33, 1.0] diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index bfa0b855005..2e7f76740d4 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2925,7 +2925,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { let layout = ContainerViewLayout( size: size, - metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact), + metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil), deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil), intrinsicInsets: intrinsicInsets, safeInsets: safeInsets, @@ -3085,28 +3085,33 @@ public final class DrawingToolsInteraction { var isRectangleImage = false var isVideo = false var isAdditional = false + var isMessage = false if let entity = entityView.entity as? DrawingStickerEntity { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true isAdditional = isAdditionalValue } else if case let .image(_, type) = entity.content, case .rectangle = type { isRectangleImage = true + } else if case .message = entity.content { + isMessage = true } } - guard !isVideo || isAdditional else { + guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else { return } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in - if let self, let entityView { - if self.shouldDeleteEntity(entityView.entity) { - self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + if !isMessage { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in + if let self, let entityView { + if self.shouldDeleteEntity(entityView.entity) { + self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + } } - } - })) + })) + } if let entityView = entityView as? DrawingLocationEntityView { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in if let self, let entityView { @@ -3121,7 +3126,7 @@ public final class DrawingToolsInteraction { self.entitiesView.selectEntity(entityView.entity) } })) - } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo { + } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Flip, accessibilityLabel: presentationData.strings.Paint_Flip), action: { [weak self] in if let self { self.flipSelectedEntity() @@ -3135,7 +3140,7 @@ public final class DrawingToolsInteraction { } })) } - if !isVideo { + if !isVideo && !isMessage { if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type { } else { @@ -3147,21 +3152,39 @@ public final class DrawingToolsInteraction { })) } } - #if DEBUG - if isRectangleImage { - actions.append(ContextMenuAction(content: .text(title: "Cut Out", accessibilityLabel: "Cut Out"), action: { [weak self, weak entityView] in + + + if #available(iOS 17.0, *), isRectangleImage { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content { let _ = (cutoutStickerImage(from: image) - |> deliverOnMainQueue).start(next: { result in - if let result { + |> deliverOnMainQueue).start(next: { [weak entity] result in + if let result, let entity { let newEntity = DrawingStickerEntity(content: .image(result, .sticker)) - self.insertEntity(newEntity) + newEntity.referenceDrawingSize = entity.referenceDrawingSize + newEntity.scale = entity.scale + newEntity.position = entity.position + newEntity.rotation = entity.rotation + newEntity.mirrored = entity.mirrored + let newEntityView = self.entitiesView.add(newEntity) + + entityView.selectionView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + if let newEntityView = newEntityView as? DrawingStickerEntityView { + newEntityView.playCutoutAnimation() + } + self.entitiesView.selectEntity(newEntity, animate: false) + newEntityView.selectionView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let entityView = entityView as? DrawingStickerEntityView { + entityView.playDissolveAnimation() + self.entitiesView.remove(uuid: entity.uuid, animated: false) + } } }) } })) } - #endif + let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0) let controller = makeContextMenuController(actions: actions) let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0) @@ -3198,6 +3221,11 @@ public final class DrawingToolsInteraction { self.insertEntity(DrawingStickerEntity(content: .image(image, isSticker ? .sticker : .rectangle)), scale: 2.5) } } + textEntityView.replaceWithAnimatedImage = { [weak self] data, thumbnailImage in + if let self { + self.insertEntity(DrawingStickerEntity(content: .animatedImage(data, thumbnailImage)), scale: 2.5) + } + } } else { if self.isVideo { entityView.seek(to: 0.0) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 5e21b7c1244..2b444556477 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -13,6 +13,7 @@ import MediaEditor import UniversalMediaPlayer import TelegramPresentationData import TelegramUniversalVideoContent +import DustEffect private class BlurView: UIVisualEffectView { private func setup() { @@ -65,6 +66,7 @@ public class DrawingStickerEntityView: DrawingEntityView { let imageNode: TransformImageNode var animationNode: DefaultAnimatedStickerNodeImpl? var videoNode: UniversalVideoNode? + var animatedImageView: UIImageView? var cameraPreviewView: UIView? let progressDisposable = MetaDisposable() @@ -118,15 +120,18 @@ public class DrawingStickerEntityView: DrawingEntityView { } func getRenderImage() -> UIImage? { - guard case let .file(_, type) = self.stickerEntity.content, case .reaction = type else { + if case let .file(_, type) = self.stickerEntity.content, case .reaction = type { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0) + self.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } else if case .message = self.stickerEntity.content { + return self.animatedImageView?.image + } else { return nil } - let rect = self.bounds - UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0) - self.drawHierarchy(in: rect, afterScreenUpdates: true) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image } private var video: TelegramMediaFile? { @@ -143,10 +148,14 @@ public class DrawingStickerEntityView: DrawingEntityView { return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case let .image(image, _): return image.size + case let .animatedImage(_, thumbnailImage): + return thumbnailImage.size case let .video(file): return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: return CGSize(width: 512.0, height: 512.0) + case let .message(_, _, size): + return size } } @@ -258,48 +267,79 @@ public class DrawingStickerEntityView: DrawingEntityView { }), attemptSynchronously: synchronous) self.setNeedsLayout() } else if case let .video(file) = self.stickerEntity.content { - let videoNode = UniversalVideoNode( - postbox: self.context.account.postbox, - audioSession: self.context.sharedContext.mediaManager.audioSession, - manager: self.context.sharedContext.mediaManager.universalVideoManager, - decoration: StickerVideoDecoration(), - content: NativeVideoContent( - id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"), - userLocation: .other, - fileReference: .standalone(media: file), - imageReference: nil, - streamVideo: .story, - loopVideo: true, - enableSound: false, - soundMuted: true, - beginWithAmbientSound: false, - mixWithOthers: true, - useLargeThumbnail: false, - autoFetchFullSizeThumbnail: false, - tempFilePath: nil, - captureProtected: false, - hintDimensions: file.dimensions?.cgSize, - storeAfterDownload: nil, - displayImage: false, - hasSentFramesToDisplay: { [weak self] in - guard let self else { - return - } - self.videoNode?.isHidden = false - } - ), - priority: .gallery - ) - videoNode.canAttachContent = true - videoNode.isUserInteractionEnabled = false - videoNode.clipsToBounds = true - self.addSubnode(videoNode) - self.videoNode = videoNode + self.setupWithVideo(file) + } else if case let .animatedImage(data, thumbnailImage) = self.stickerEntity.content { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = thumbnailImage + imageView.setDrawingAnimatedImage(data: data) + self.animatedImageView = imageView + self.addSubview(imageView) self.setNeedsLayout() - videoNode.play() + } else if case .message = self.stickerEntity.content { + if let image = self.stickerEntity.renderImage { + self.setupWithImage(image) + } } } + private func setupWithImage(_ image: UIImage) { + let imageView: UIImageView + if let current = self.animatedImageView { + imageView = current + } else { + imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + self.addSubview(imageView) + self.animatedImageView = imageView + } + imageView.image = image + self.currentSize = nil + self.setNeedsLayout() + } + + private func setupWithVideo(_ file: TelegramMediaFile) { + let videoNode = UniversalVideoNode( + postbox: self.context.account.postbox, + audioSession: self.context.sharedContext.mediaManager.audioSession, + manager: self.context.sharedContext.mediaManager.universalVideoManager, + decoration: StickerVideoDecoration(), + content: NativeVideoContent( + id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"), + userLocation: .other, + fileReference: .standalone(media: file), + imageReference: nil, + streamVideo: .story, + loopVideo: true, + enableSound: false, + soundMuted: true, + beginWithAmbientSound: false, + mixWithOthers: true, + useLargeThumbnail: false, + autoFetchFullSizeThumbnail: false, + tempFilePath: nil, + captureProtected: false, + hintDimensions: file.dimensions?.cgSize, + storeAfterDownload: nil, + displayImage: false, + hasSentFramesToDisplay: { [weak self] in + guard let self else { + return + } + self.videoNode?.isHidden = false + } + ), + priority: .gallery + ) + videoNode.canAttachContent = true + videoNode.isUserInteractionEnabled = false + videoNode.clipsToBounds = true + self.addSubnode(videoNode) + self.videoNode = videoNode + self.setNeedsLayout() + videoNode.play() + } + public override func play() { self.isVisible = true self.applyVisibility() @@ -333,6 +373,12 @@ public class DrawingStickerEntityView: DrawingEntityView { self.applyVisibility() } + public var isNightTheme = false { + didSet { + self.animatedImageView?.image = self.isNightTheme ? self.stickerEntity.secondaryRenderImage : self.stickerEntity.renderImage + } + } + func applyVisibility() { let isPlaying = self.isVisible if self.isPlaying != isPlaying { @@ -460,6 +506,48 @@ public class DrawingStickerEntityView: DrawingEntityView { } } + public func playDissolveAnimation(completion: @escaping () -> Void = {}) { + guard let containerView = self.containerView, case let .image(image, _) = self.stickerEntity.content else { + return + } + + let scaledSize = image.size.aspectFitted(CGSize(width: 180.0, height: 180.0)) + guard let scaledImage = generateScaledImage(image: image, size: scaledSize) else { + self.isHidden = true + completion() + return + } + + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = self.center + dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: containerView.bounds.size) + containerView.layer.insertSublayer(dustEffectLayer, below: self.layer) + + dustEffectLayer.animationSpeed = 2.2 + dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in + dustEffectLayer?.removeFromSuperlayer() + completion() + } + + let maxSize = CGSize(width: 512.0, height: 512.0) + let itemSize = CGSize(width: self.bounds.width * self.entity.scale, height: self.bounds.height * self.entity.scale) + let fittedSize = itemSize.aspectFittedOrSmaller(maxSize) + let scale = itemSize.width / fittedSize.width + + dustEffectLayer.transform = CATransform3DScale(CATransform3DMakeRotation(self.stickerEntity.rotation, 0.0, 0.0, 1.0), scale, scale, 1.0) + + let itemFrame = CGRect(origin: CGPoint(x: (containerView.bounds.width - fittedSize.width) / 2.0, y: (containerView.bounds.height - fittedSize.height) / 2.0), size: fittedSize) + dustEffectLayer.addItem(frame: itemFrame, image: scaledImage) + + self.isHidden = true + } + + public func playCutoutAnimation() { + let values = [self.entity.scale, self.entity.scale * 1.1, self.entity.scale] + let keyTimes = [0.0, 0.67, 1.0] + self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.35, keyPath: "transform.scale") + } + private var didApplyVisibility = false public override func layoutSubviews() { super.layoutSubviews() @@ -491,11 +579,22 @@ public class DrawingStickerEntityView: DrawingEntityView { } if let videoNode = self.videoNode { - videoNode.cornerRadius = floor(imageSize.width * 0.03) + var imageSize = imageSize + if case let .message(_, file, _) = self.stickerEntity.content, let dimensions = file?.dimensions { + let fittedDimensions = dimensions.cgSize.aspectFitted(boundingSize) + imageSize = fittedDimensions + videoNode.cornerRadius = 0.0 + } else { + videoNode.cornerRadius = floor(imageSize.width * 0.03) + } videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) videoNode.updateLayout(size: imageSize, transition: .immediate) } + if let animatedImageView = self.animatedImageView { + animatedImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) + } + if let cameraPreviewView = self.cameraPreviewView { cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0 cameraPreviewView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) @@ -531,6 +630,13 @@ public class DrawingStickerEntityView: DrawingEntityView { self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) self.updateAnimationColor() + + if case .message = self.stickerEntity.content, self.animatedImageView == nil { + let image = self.isNightTheme ? self.stickerEntity.secondaryRenderImage : self.stickerEntity.renderImage + if let image { + self.setupWithImage(image) + } + } self.updateMirroring(animated: animated) @@ -557,15 +663,18 @@ public class DrawingStickerEntityView: DrawingEntityView { self.imageNode.transform = animationSourceTransform self.animationNode?.transform = animationSourceTransform self.videoNode?.transform = animationSourceTransform + self.animatedImageView?.layer.transform = animationSourceTransform UIView.animate(withDuration: 0.25, animations: { self.imageNode.transform = animationTargetTransform self.animationNode?.transform = animationTargetTransform self.videoNode?.transform = animationTargetTransform + self.animatedImageView?.layer.transform = animationTargetTransform }, completion: { finished in self.imageNode.transform = staticTransform self.animationNode?.transform = staticTransform self.videoNode?.transform = staticTransform + self.animatedImageView?.layer.transform = staticTransform }) } else { CATransaction.begin() @@ -573,6 +682,7 @@ public class DrawingStickerEntityView: DrawingEntityView { self.imageNode.transform = staticTransform self.animationNode?.transform = staticTransform self.videoNode?.transform = staticTransform + self.animatedImageView?.layer.transform = staticTransform CATransaction.commit() } } @@ -605,6 +715,15 @@ public class DrawingStickerEntityView: DrawingEntityView { selectionView.entityView = self return selectionView } + + func getRenderSubEntities() -> [DrawingEntity] { + guard case let .message(_, file, _) = self.stickerEntity.content else { + return [] + } + + let _ = file + return [] + } } final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { @@ -882,9 +1001,17 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { actualInset = floorToScreenPixels((self.bounds.width - width) / 2.0) - let cornerRadius: CGFloat = 12.0 - self.scale + var cornerRadius: CGFloat = 12.0 - self.scale + var count = 12 + if case .message = entity.content { + cornerRadius *= 2.1 + count = 24 + } else if case .image = entity.content { + count = 24 + } + let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) - let count = 12 + let dashLength = perimeter / CGFloat(count) self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] @@ -899,18 +1026,12 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath } - let handles = [ - self.leftHandle, - self.rightHandle - ] - - for handle in handles { + for handle in [self.leftHandle, self.rightHandle] { handle.path = handlePath handle.bounds = bounds handle.lineWidth = lineWidth } - self.leftHandle.position = CGPoint(x: actualInset, y: self.bounds.midY) self.rightHandle.position = CGPoint(x: self.bounds.maxX - actualInset, y: self.bounds.midY) } @@ -1029,3 +1150,156 @@ private final class StickerVideoDecoration: UniversalVideoDecoration { public func tap() { } } + +private extension UIBezierPath { + static func smoothCurve( + through points: [CGPoint], + length: CGFloat + ) -> UIBezierPath { + let angle = (CGFloat.pi * 2) / CGFloat(points.count) + let smoothness: CGFloat = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 + + var smoothPoints = [SmoothPoint]() + for index in (0 ..< points.count) { + let prevIndex = index - 1 + let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex] + let curr = points[index] + let next = points[(index + 1) % points.count] + + let angle: CGFloat = { + let dx = next.x - prev.x + let dy = -next.y + prev.y + let angle = atan2(dy, dx) + if angle < 0 { + return abs(angle) + } else { + return 2 * .pi - angle + } + }() + + smoothPoints.append( + SmoothPoint( + point: curr, + inAngle: angle + .pi, + inLength: smoothness * distance(from: curr, to: prev), + outAngle: angle, + outLength: smoothness * distance(from: curr, to: next) + ) + ) + } + + let resultPath = UIBezierPath() + resultPath.move(to: smoothPoints[0].point) + for index in (0 ..< smoothPoints.count) { + let curr = smoothPoints[index] + let next = smoothPoints[(index + 1) % points.count] + let currSmoothOut = curr.smoothOut() + let nextSmoothIn = next.smoothIn() + resultPath.addCurve(to: next.point, controlPoint1: currSmoothOut, controlPoint2: nextSmoothIn) + } + resultPath.close() + return resultPath + } + + static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat { + return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y)) + } + + struct SmoothPoint { + let point: CGPoint + + let inAngle: CGFloat + let inLength: CGFloat + + let outAngle: CGFloat + let outLength: CGFloat + + func smoothIn() -> CGPoint { + return smooth(angle: inAngle, length: inLength) + } + + func smoothOut() -> CGPoint { + return smooth(angle: outAngle, length: outLength) + } + + private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint { + return CGPoint( + x: point.x + length * cos(angle), + y: point.y + length * sin(angle) + ) + } + } +} + + + +extension UIImageView { + func setDrawingAnimatedImage(data: Data) { + DispatchQueue.global().async { + if let animatedImage = UIImage.animatedImageFromData(data: data) { + DispatchQueue.main.async { + self.setImage(with: animatedImage) + self.startAnimating() + } + } + } + } + + private func setImage(with animatedImage: DrawingAnimatedImage) { + if let snapshotView = self.snapshotView(afterScreenUpdates: false) { + self.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + self.image = nil + self.animationImages = animatedImage.images + self.animationDuration = animatedImage.duration + self.animationRepeatCount = 0 + } +} + +//private func prerenderEntityTransformations(entity: DrawingEntity, image: UIImage, colorSpace: CGColorSpace) -> UIImage { +// let imageSize = image.size +// +// let angle: CGFloat +// var scale: CGFloat +// let position: CGPoint +// +// if let entity = entity as? DrawingStickerEntity { +// angle = -entity.rotation +// scale = entity.scale +// position = entity.position +// } else { +// fatalError() +// } +// +// let rotatedSize = CGSize( +// width: abs(imageSize.width * cos(angle)) + abs(imageSize.height * sin(angle)), +// height: abs(imageSize.width * sin(angle)) + abs(imageSize.height * cos(angle)) +// ) +// let newSize = CGSize(width: rotatedSize.width * scale, height: rotatedSize.height * scale) +// +// let newImage = generateImage(newSize, contextGenerator: { size, context in +// context.setAllowsAntialiasing(true) +// context.setShouldAntialias(true) +// context.interpolationQuality = .high +// context.clear(CGRect(origin: .zero, size: size)) +// context.translateBy(x: newSize.width * 0.5, y: newSize.height * 0.5) +// context.rotate(by: angle) +// context.scaleBy(x: scale, y: scale) +// let drawRect = CGRect( +// x: -imageSize.width * 0.5, +// y: -imageSize.height * 0.5, +// width: imageSize.width, +// height: imageSize.height +// ) +// if let cgImage = image.cgImage { +// context.draw(cgImage, in: drawRect) +// } +// }, scale: 1.0)! +// +// let _ = position +// +// return newImage +//} diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index c062142fa09..3351c8ae675 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -34,6 +34,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate var textChanged: () -> Void = {} var replaceWithImage: (UIImage, Bool) -> Void = { _, _ in } + var replaceWithAnimatedImage: (Data, UIImage) -> Void = { _, _ in } init(context: AccountContext, entity: DrawingTextEntity) { self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true) @@ -103,8 +104,13 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate var images: [UIImage] = [] var isPNG = false var isMemoji = false + var animatedImageData: Data? for item in pasteboard.items { - if let image = item["com.apple.png-sticker"] as? UIImage { + print(item.keys) + if let data = item["public.heics"] as? Data, let image = item[kUTTypePNG as String] as? UIImage { + animatedImageData = data + images.append(image) + } else if let imageData = item["com.apple.png-sticker"] as? Data, let image = UIImage(data: imageData) { images.append(image) isPNG = true isMemoji = true @@ -121,6 +127,12 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } } + if let animatedImageData, let image = images.first { + self.endEditing(reset: true) + self.replaceWithAnimatedImage(animatedImageData, image) + return false + } + if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage { let maxSide = max(image.size.width, image.size.height) if maxSide.isZero { diff --git a/submodules/DrawingUI/Sources/DrawingUtils.swift b/submodules/DrawingUI/Sources/DrawingUtils.swift index 43f60470689..1050beaafbe 100644 --- a/submodules/DrawingUI/Sources/DrawingUtils.swift +++ b/submodules/DrawingUI/Sources/DrawingUtils.swift @@ -544,3 +544,59 @@ extension CATransform3D { return (t, r, s) } } + +public extension UIImage { + class func animatedImageFromData(data: Data) -> DrawingAnimatedImage? { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + return nil + } + + let count = CGImageSourceGetCount(source) + var images = [UIImage]() + var duration = 0.0 + + for i in 0.. Double { + var delay = 0.0 + + let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) + let gifPropertiesPointer = UnsafeMutablePointer.allocate(capacity: 0) + if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false { + return delay + } + + let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self) + + var delayObject: AnyObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()), to: AnyObject.self) + if delayObject.doubleValue == 0 { + delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self) + } + + delay = delayObject as? Double ?? 0 + + return delay + } +} + +public final class DrawingAnimatedImage { + public let images: [UIImage] + public let duration: Double + + init(images: [UIImage], duration: Double) { + self.images = images + self.duration = duration + } +} diff --git a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift b/submodules/DrawingUI/Sources/ImageObjectSeparation.swift index 5d7e8b6146e..c27efa81de0 100644 --- a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift +++ b/submodules/DrawingUI/Sources/ImageObjectSeparation.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Vision import CoreImage +import CoreImage.CIFilterBuiltins import SwiftSignalKit import VideoToolbox @@ -13,6 +14,8 @@ func cutoutStickerImage(from image: UIImage) -> Signal { return .single(nil) } return Signal { subscriber in + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { @@ -21,13 +24,20 @@ func cutoutStickerImage(from image: UIImage) -> Signal { return } let instances = instances(atPoint: nil, inObservation: result) - if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler), let image = UIImage(pixelBuffer: mask) { - subscriber.putNext(image) - subscriber.putCompletion() - } else { - subscriber.putNext(nil) - subscriber.putCompletion() + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(image) + subscriber.putCompletion() + return + } } + subscriber.putNext(nil) + subscriber.putCompletion() } try? handler.perform([request]) return ActionDisposable { diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 52f4a47a76c..b10ac864ab1 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -1624,7 +1624,10 @@ public class StickerPickerScreen: ViewController { modalProgress = 0.0 } self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) - + if self.isDismissing { + return + } + let clipFrame: CGRect let contentFrame: CGRect if layout.metrics.widthClass == .compact { diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index e30a0e09c3c..690a82aebc8 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -68,37 +68,14 @@ public extension String { var isSingleEmoji: Bool { return self.count == 1 && self.containsEmoji -// return self.emojis.count == 1 && self.containsEmoji } var containsEmoji: Bool { return self.contains { $0.isEmoji } - //return self.unicodeScalars.contains { $0.isEmoji } } var containsOnlyEmoji: Bool { return !self.isEmpty && !self.contains { !$0.isEmoji } -// guard !self.isEmpty else { -// return false -// } -// var nextShouldBeVariationSelector = false -// for scalar in self.unicodeScalars { -// if nextShouldBeVariationSelector { -// if scalar == UnicodeScalar.VariationSelector { -// nextShouldBeVariationSelector = false -// continue -// } else { -// return false -// } -// } -// if !scalar.isEmoji && scalar.maybeEmoji { -// nextShouldBeVariationSelector = true -// } -// else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner { -// return false -// } -// } -// return !nextShouldBeVariationSelector } var emojis: [String] { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index ca46f6d3567..2ed45cdf2c4 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -95,11 +95,13 @@ public final class HashtagSearchController: TelegramBaseController { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openPremiumGift: { }, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 62b03688e1c..90e2e8e277c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -575,6 +575,8 @@ private final class PendingInAppPurchaseState: Codable { case additionalPeerIds case countries case onlyNewSubscribers + case showWinners + case prizeDescription case randomId case untilDate } @@ -593,7 +595,7 @@ private final class PendingInAppPurchaseState: Codable { case restore case gift(peerId: EnginePeer.Id) case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?) - case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) + case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -621,6 +623,8 @@ private final class PendingInAppPurchaseState: Codable { additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) }, countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [], onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers), + showWinners: try container.decodeIfPresent(Bool.self, forKey: .showWinners) ?? false, + prizeDescription: try container.decodeIfPresent(String.self, forKey: .prizeDescription), randomId: try container.decode(Int64.self, forKey: .randomId), untilDate: try container.decode(Int32.self, forKey: .untilDate) ) @@ -646,12 +650,14 @@ private final class PendingInAppPurchaseState: Codable { try container.encode(PurposeType.giftCode.rawValue, forKey: .type) try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers) try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer) - case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate): + case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): try container.encode(PurposeType.giveaway.rawValue, forKey: .type) try container.encode(boostPeer.toInt64(), forKey: .boostPeer) try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds) try container.encode(countries, forKey: .countries) try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers) + try container.encode(showWinners, forKey: .showWinners) + try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription) try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) } @@ -669,8 +675,8 @@ private final class PendingInAppPurchaseState: Codable { self = .gift(peerId: peerId) case let .giftCode(peerIds, boostPeer, _, _): self = .giftCode(peerIds: peerIds, boostPeer: boostPeer) - case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, _, _): - self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate) + case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _): + self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) } } @@ -687,8 +693,8 @@ private final class PendingInAppPurchaseState: Codable { return .gift(peerId: peerId, currency: currency, amount: amount) case let .giftCode(peerIds, boostPeer): return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount) - case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate): - return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) + case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): + return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) } } } diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 3bd18d48523..591f0177488 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -24,19 +24,21 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { let icon: UIImage? let iconSignal: Signal? let title: String + let additionalBadgeIcon: UIImage? public let alwaysPlain: Bool let hasSeparator: Bool let editing: Bool let height: ItemListPeerActionItemHeight let color: ItemListPeerActionItemColor public let sectionId: ItemListSectionId - let action: (() -> Void)? + public let action: (() -> Void)? - public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal? = nil, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) { + public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) { self.presentationData = presentationData self.icon = icon self.iconSignal = iconSignal self.title = title + self.additionalBadgeIcon = additionalBadgeIcon self.alwaysPlain = alwaysPlain self.hasSeparator = hasSeparator self.editing = editing @@ -102,7 +104,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { } } -class ItemListPeerActionItemNode: ListViewItemNode { +public final class ItemListPeerActionItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -111,6 +113,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { private let iconNode: ASImageNode private let titleNode: TextNode + private var additionalLabelBadgeNode: ASImageNode? private let activateArea: AccessibilityAreaNode @@ -118,7 +121,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { private let iconDisposable = MetaDisposable() - init() { + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -157,7 +160,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { self.iconDisposable.dispose() } - func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + public func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item @@ -177,7 +180,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { switch item.height { case .generic: iconOffset = 1.0 - verticalInset = 11.0 + verticalInset = 12.0 verticalOffset = 0.0 leftInset = (item.icon == nil && item.iconSignal == nil ? 16.0 : 59.0) + params.leftInset case .peerList: @@ -296,15 +299,37 @@ class ItemListPeerActionItemNode: ListViewItemNode { 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, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size) + transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + + if let additionalBadgeIcon = item.additionalBadgeIcon { + let additionalLabelBadgeNode: ASImageNode + if let current = strongSelf.additionalLabelBadgeNode { + additionalLabelBadgeNode = current + } else { + additionalLabelBadgeNode = ASImageNode() + additionalLabelBadgeNode.isUserInteractionEnabled = false + strongSelf.additionalLabelBadgeNode = additionalLabelBadgeNode + strongSelf.addSubnode(additionalLabelBadgeNode) + } + additionalLabelBadgeNode.image = additionalBadgeIcon + + let additionalLabelSize = additionalBadgeIcon.size + additionalLabelBadgeNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 6.0, y: floor((contentSize.height - additionalLabelSize.height) / 2.0)), size: additionalLabelSize) + } else { + if let additionalLabelBadgeNode = strongSelf.additionalLabelBadgeNode { + strongSelf.additionalLabelBadgeNode = nil + additionalLabelBadgeNode.removeFromSupernode() + } + } } }) } } - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + public override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) if highlighted { @@ -342,11 +367,11 @@ class ItemListPeerActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + public override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + public override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 093c11e33ed..7a17a43cf39 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -913,7 +913,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = item.peer.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 093ed9b76cb..3ce8365f56e 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -591,7 +591,7 @@ open class ItemListControllerNode: ASDisplayNode { var footerHeight: CGFloat = 0.0 if let footerItemNode = self.footerItemNode { footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) - insets.bottom = footerHeight + insets.bottom = max(footerHeight, insets.bottom) } self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index 7cede4afcb4..1cadb4b7d70 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -122,6 +122,10 @@ public func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEd } public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ params: ListViewItemLayoutParams) -> UIEdgeInsets { + if params.isStandalone { + return UIEdgeInsets() + } + let topInset: CGFloat switch neighbors.top { case .none: diff --git a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift index 047594a9265..518e61d1082 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift @@ -52,7 +52,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem { Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply() }) + return (nil, { _ in apply(false) }) }) } } @@ -67,7 +67,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in - apply() + apply(false) }) } } @@ -130,7 +130,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.activateArea) } - public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item @@ -179,7 +179,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - return (layout, { [weak self] in + return (layout, { [weak self] _ in if let strongSelf = self { strongSelf.item = item diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerSendActionSheetController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerSendActionSheetController.h index e75e0da4f18..708276456ce 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerSendActionSheetController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerSendActionSheetController.h @@ -4,6 +4,9 @@ NS_ASSUME_NONNULL_BEGIN @interface TGMediaPickerSendActionSheetController : TGOverlayController +// MARK: Nicegram RoundedVideos +@property (nonatomic, copy) void (^sendAsRoundedVideo)(void); +// @property (nonatomic, copy) void (^send)(void); @property (nonatomic, copy) void (^sendSilently)(void); @property (nonatomic, copy) void (^sendWhenOnline)(void); @@ -11,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) void (^sendWithTimer)(void); - (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSendWhenOnline:(bool)canSendWhenOnline canSchedule:(bool)canSchedule reminder:(bool)reminder hasTimer:(bool)hasTimer; +// MARK: Nicegram RoundedVideos +- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendAsRoundedVideo:(bool)canSendAsRoundedVideo canSendSilently:(bool)canSendSilently canSendWhenOnline:(bool)canSendWhenOnline canSchedule:(bool)canSchedule reminder:(bool)reminder hasTimer:(bool)hasTimer; +// @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h index 4ea98cdf5f7..ed78e3cbef0 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGModernConversationInputMicButton.h @@ -88,5 +88,6 @@ - (void)_commitLocked; - (void)setHidesPanelOnLock; +- (UIView *)createLockPanelView; @end diff --git a/submodules/LegacyComponents/Sources/NGLegacyComponents.h b/submodules/LegacyComponents/Sources/NGLegacyComponents.h new file mode 100644 index 00000000000..08f09ee1b0e --- /dev/null +++ b/submodules/LegacyComponents/Sources/NGLegacyComponents.h @@ -0,0 +1,3 @@ +#import + +NSString *NGLocalized(NSString *key); diff --git a/submodules/LegacyComponents/Sources/NGLegacyComponents.m b/submodules/LegacyComponents/Sources/NGLegacyComponents.m new file mode 100644 index 00000000000..01ba03fa72d --- /dev/null +++ b/submodules/LegacyComponents/Sources/NGLegacyComponents.m @@ -0,0 +1,18 @@ +#import + +#import "LegacyComponentsInternal.h" + +#import "TGLocalization.h" + +NSString *NGLocalized(NSString *key) { + TGLocalization *localization = [[LegacyComponentsGlobals provider] effectiveLocalization]; + NSString *code = localization.code; + NSString *table = @"NiceLocalizable"; + + NSBundle *enBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]]; + NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:code ofType:@"lproj"]]; + + NSString *enString = [enBundle localizedStringForKey:key value:key table:table]; + + return [bundle localizedStringForKey:key value:enString table:table]; +} diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerSendActionSheetController.m b/submodules/LegacyComponents/Sources/TGMediaPickerSendActionSheetController.m index e54924dd5bb..d5f9c6ee5cf 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerSendActionSheetController.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerSendActionSheetController.m @@ -1,3 +1,7 @@ +// MARK: Nicegram RoundedVideos +#import "NGLegacyComponents.h" +// + #import "TGMediaPickerSendActionSheetController.h" #import "LegacyComponentsInternal.h" @@ -102,6 +106,11 @@ @interface TGMediaPickerSendActionSheetController () { id _context; + // MARK: Nicegram RoundedVideos + bool _canSendAsRoundedVideo; + TGMediaPickerSendActionSheetItemView *_sendAsRoundedVideoButton; + // + bool _isDark; CGRect _sendButtonFrame; bool _canSendSilently; @@ -126,12 +135,16 @@ @interface TGMediaPickerSendActionSheetController () @implementation TGMediaPickerSendActionSheetController -- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSendWhenOnline:(bool)canSendWhenOnline canSchedule:(bool)canSchedule reminder:(bool)reminder hasTimer:(bool)hasTimer { +// MARK: Nicegram RoundedVideos, canSendAsRoundedVideo added +- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendAsRoundedVideo:(bool)canSendAsRoundedVideo canSendSilently:(bool)canSendSilently canSendWhenOnline:(bool)canSendWhenOnline canSchedule:(bool)canSchedule reminder:(bool)reminder hasTimer:(bool)hasTimer { self = [super initWithContext:context]; if (self != nil) { _context = context; _isDark = isDark; _sendButtonFrame = sendButtonFrame; + // MARK: Nicegram RoundedVideos + _canSendAsRoundedVideo = canSendAsRoundedVideo; + // _canSendSilently = canSendSilently; _canSendWhenOnline = canSendWhenOnline; _canSchedule = canSchedule; @@ -141,6 +154,12 @@ - (instancetype)initWithContext:(id)context isDark:(boo return self; } +// MARK: Nicegram RoundedVideos +- (instancetype)initWithContext:(id)context isDark:(bool)isDark sendButtonFrame:(CGRect)sendButtonFrame canSendSilently:(bool)canSendSilently canSendWhenOnline:(bool)canSendWhenOnline canSchedule:(bool)canSchedule reminder:(bool)reminder hasTimer:(bool)hasTimer { + return [self initWithContext:context isDark:isDark sendButtonFrame:sendButtonFrame canSendAsRoundedVideo:false canSendSilently:canSendSilently canSendWhenOnline:canSendWhenOnline canSchedule:canSchedule reminder:reminder hasTimer:hasTimer]; +} +// + - (void)loadView { [super loadView]; @@ -165,6 +184,23 @@ - (void)loadView { [self.view addSubview:_containerView]; __weak TGMediaPickerSendActionSheetController *weakSelf = self; + + // MARK: Nicegram RoundedVideos + if (_canSendAsRoundedVideo) { + UIImage *icon; + if (@available(iOS 13.0, *)) { + icon = [UIImage systemImageNamed:@"video.circle"]; + } + + _sendAsRoundedVideoButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:NGLocalized(@"RoundedVideos.ButtonTitle") icon:icon isDark:_isDark isLast: !_canSendSilently && !_canSchedule && !_hasTimer && !_canSendWhenOnline]; + _sendAsRoundedVideoButton.pressed = ^{ + __strong TGMediaPickerSendActionSheetController *strongSelf = weakSelf; + [strongSelf sendAsRoundedVideoPressed]; + }; + [_containerView addSubview:_sendAsRoundedVideoButton]; + } + // + if (_canSendSilently) { _sendSilentlyButton = [[TGMediaPickerSendActionSheetItemView alloc] initWithTitle:TGLocalized(@"Conversation.SendMessage.SendSilently") icon:TGComponentsImageNamed(@"Editor/Silently") isDark:_isDark isLast:!_canSchedule && !_hasTimer && !_canSendWhenOnline]; _sendSilentlyButton.pressed = ^{ @@ -307,13 +343,21 @@ - (void)viewDidLayoutSubviews { CGFloat itemHeight = 44.0; CGFloat containerWidth = 240.0; - CGFloat containerHeight = (_canSendSilently + _canSchedule + _hasTimer + _canSendWhenOnline) * itemHeight; - containerWidth = MAX(containerWidth, MAX(_timerButton.buttonLabel.frame.size.width, MAX(_sendSilentlyButton.buttonLabel.frame.size.width, MAX(_sendWhenOnlineButton.buttonLabel.frame.size.width, _scheduleButton.buttonLabel.frame.size.width))) + 84.0); + // MARK: Nicegram RoundedVideos, add _canSendAsRoundedVideo + CGFloat containerHeight = (_canSendAsRoundedVideo + _canSendSilently + _canSchedule + _hasTimer + _canSendWhenOnline) * itemHeight; + // MARK: Nicegram RoundedVideos, _sendAsRoundedVideoButton added + containerWidth = MAX(containerWidth, MAX(_sendAsRoundedVideoButton.buttonLabel.frame.size.width, MAX(_timerButton.buttonLabel.frame.size.width, MAX(_sendSilentlyButton.buttonLabel.frame.size.width, MAX(_sendWhenOnlineButton.buttonLabel.frame.size.width, _scheduleButton.buttonLabel.frame.size.width)))) + 84.0); if (!_dismissed) { _containerView.frame = CGRectMake(CGRectGetMaxX(_sendButtonFrame) - containerWidth - 8.0, _sendButtonFrame.origin.y - containerHeight - 4.0, containerWidth, containerHeight); } CGFloat offset = 0.0f; + + // MARK: Nicegram RoundedVideos + _sendAsRoundedVideoButton.frame = CGRectMake(0.0, offset, containerWidth, itemHeight); + offset += _sendAsRoundedVideoButton.frame.size.height; + // + _sendSilentlyButton.frame = CGRectMake(0.0, offset, containerWidth, itemHeight); offset += _sendSilentlyButton.frame.size.height; @@ -326,6 +370,15 @@ - (void)viewDidLayoutSubviews { _timerButton.frame = CGRectMake(0.0, offset, containerWidth, itemHeight); } +// MARK: Nicegram RoundedVideos +- (void)sendAsRoundedVideoPressed { + [self animateOut:false]; + + if (self.sendAsRoundedVideo != nil) + self.sendAsRoundedVideo(); +} +// + - (void)sendPressed { _sendButton.enabled = false; diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index bf73ed470e5..7161bb73140 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -115,7 +115,7 @@ @interface TGModernConversationInputMicButton () UIImageView *_innerIconView; UIView *_lockPanelWrapperView; - UIImageView *_lockPanelView; + UIView *_lockPanelView; UIImageView *_lockArrowView; TGModernConversationInputLockView *_lockView; UIImage *_previousIcon; @@ -265,7 +265,9 @@ - (void)setPallete:(TGModernConversationInputMicPallete *)pallete { if (!update) return; - _lockPanelView.image = [self panelBackgroundImage]; + if ([_lockPanelView isKindOfClass:[UIImageView class]]) { + ((UIImageView *)_lockPanelView).image = [self panelBackgroundImage]; + } _lockArrowView.image = TGTintedImage(TGComponentsImageNamed(@"VideoRecordArrow"), self.pallete != nil ? self.pallete.lockColor : UIColorRGB(0x9597a0)); _lockView.color = self.pallete.lockColor; @@ -341,6 +343,13 @@ - (UIImage *)stopButtonImage return stopButtonImage; } +- (UIView *)createLockPanelView { + UIImageView *view = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; + view.userInteractionEnabled = true; + view.image = [self panelBackgroundImage]; + return view; +} + - (void)animateIn { if (!_locked) { _lockView.lockness = 0.0f; @@ -373,9 +382,7 @@ - (void)animateIn { _lockPanelWrapperView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; [[_presentation view] addSubview:_lockPanelWrapperView]; - _lockPanelView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 40.0f, 72.0f)]; - _lockPanelView.userInteractionEnabled = true; - _lockPanelView.image = [self panelBackgroundImage]; + _lockPanelView = [self createLockPanelView]; [_lockPanelWrapperView addSubview:_lockPanelView]; diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index b919be5465a..63dd5dde657 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +NGDEPS = [ + "//Nicegram/NGRoundedVideos:NGRoundedVideos", +] + swift_library( name = "LegacyMediaPickerUI", module_name = "LegacyMediaPickerUI", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = NGDEPS + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 14b8218342c..f82839b3a51 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -1,3 +1,6 @@ +// MARK: Nicegram RoundedVideos +import NGRoundedVideos +// import Foundation import UIKit import LegacyComponents @@ -352,11 +355,117 @@ public struct LegacyAssetPickerEnqueueMessage { public var isFile: Bool } +// MARK: Nicegram RoundedVideos +private func ngMapLegacyAssetPickerValues( + _ values: Any?, + sendAsRoundedVideo: Bool +) -> Any? { + guard sendAsRoundedVideo else { + return values + } + + var result: [Any] = [] + for value in (values as! NSArray) { + if let itemWrapper = (value as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper, + case let .video(data, thumbnail, adjustments, caption, asFile, asAnimation, stickers) = itemWrapper.item { + let trimTimeRange: CMTimeRange + if let adjustments, + !adjustments.trimTimeRange().isEmpty { + trimTimeRange = adjustments.trimTimeRange() + } else { + let duration: Double + switch data { + case let .asset(asset): + duration = asset.videoDuration + case let .tempFile(_, _, _duration): + duration = _duration + } + trimTimeRange = CMTimeRange( + start: .zero, + duration: CMTime( + seconds: duration, + preferredTimescale: 60 + ) + ) + } + + let originalSize: CGSize + if let adjustments { + originalSize = adjustments.originalSize + } else { + switch data { + case let .asset(asset): + originalSize = asset.dimensions + case let .tempFile(_, _dimensions, _): + originalSize = _dimensions + } + } + + let chunks = NGRoundedVideos.trim(range: trimTimeRange) + + for (index, chunk) in chunks.enumerated() { + let chunkAdjustments = TGVideoEditAdjustments( + originalSize: originalSize, + cropRect: adjustments?.cropRect ?? .zero, + cropOrientation: adjustments?.cropOrientation ?? .up, + cropRotation: adjustments?.cropRotation ?? .zero, + cropLockedAspectRatio: adjustments?.cropLockedAspectRatio ?? 1, + cropMirrored: adjustments?.cropMirrored ?? false, + trimStartValue: chunk.start.seconds, + trimEndValue: chunk.end.seconds, + toolValues: adjustments?.toolValues, + paintingData: adjustments?.paintingData, + sendAsGif: adjustments?.sendAsGif ?? false, + preset: TGMediaVideoConversionPresetVideoMessage + ) + + var chunkUniqueId = itemWrapper.uniqueId + chunkUniqueId?.append("-\(index)") + + let chunkCaption: NSAttributedString? = if index == chunks.startIndex { + caption + } else { + nil + } + + let newValue: NSDictionary = [ + "item": LegacyAssetItemWrapper( + item: .video(data: data, thumbnail: thumbnail, adjustments: chunkAdjustments, caption: chunkCaption, asFile: asFile, asAnimation: asAnimation, stickers: stickers), + timer: itemWrapper.timer, + spoiler: itemWrapper.spoiler, + groupedId: nil, + uniqueId: chunkUniqueId + ) + ] + result.append(newValue) + } + } else { + result.append(value) + } + } + + return result +} +// + public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: Account, signals: [Any]) -> Signal<[LegacyAssetPickerEnqueueMessage], Void> { + + // MARK: Nicegram RoundedVideos + let sendAsRoundedVideo = NGRoundedVideos.sendAsRoundedVideo + NGRoundedVideos.sendAsRoundedVideo = false + // + return Signal { subscriber in let disposable = SSignal.combineSignals(signals).start(next: { anyValues in var messages: [LegacyAssetPickerEnqueueMessage] = [] + // MARK: Nicegram RoundedVideos + let anyValues = ngMapLegacyAssetPickerValues( + anyValues, + sendAsRoundedVideo: sendAsRoundedVideo + ) + // + outer: for item in (anyValues as! NSArray) { if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper { switch item.item { @@ -751,8 +860,22 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A if asAnimation { fileAttributes.append(.Animated) } + + // MARK: Nicegram RoundedVideos + let videoFlags: TelegramMediaVideoFlags = if sendAsRoundedVideo { + [.instantRoundVideo] + } else { + [.supportsStreaming] + } + + if sendAsRoundedVideo { + finalDimensions = NGRoundedVideos.Constants.videoSize + } + // + if !asFile { - fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.supportsStreaming], preloadSize: nil)) + // MARK: Nicegram RoundedVideos, change to 'flags: videoFlags' + fileAttributes.append(.Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: videoFlags, preloadSize: nil)) if let adjustments = adjustments { if adjustments.sendAsGif { fileAttributes.append(.Animated) @@ -811,8 +934,37 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A } } } + + // MARK: Nicegram RoundedVideos + let localGroupingKey: Int64? = if sendAsRoundedVideo { + nil + } else { + item.groupedId + } + + let messageText = if sendAsRoundedVideo { + "" + } else { + text.string + } + // - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) + // MARK: Nicegram RoundedVideos, change (text, localGroupingKey) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: messageText, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) + + // MARK: Nicegram RoundedVideos + if sendAsRoundedVideo { + let videoMessageText = text + .string + .trimmingCharacters(in: .whitespacesAndNewlines) + + if !videoMessageText.isEmpty { + var uniqueId = item.uniqueId + uniqueId?.append("-text") + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: videoMessageText, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: localGroupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []), uniqueId: uniqueId, isFile: false)) + } + } + // } } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 95033c420f9..e749b0f6ea3 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -146,10 +146,14 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image, _): self.file = nil self.imagePromise.set(.single(image)) + case .animatedImage: + self.file = nil case .video: self.file = nil case .dualVideoReference: self.file = nil + case .message: + self.file = nil } } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift index a556175eeaf..38ab335d8c0 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyWallpaperPicker.swift @@ -2,10 +2,12 @@ import Foundation import UIKit import Display import SwiftSignalKit +import TelegramCore import LegacyComponents import TelegramPresentationData import DeviceAccess import AccountContext +import LocalMediaResources public func legacyWallpaperPicker(context: AccountContext, presentationData: PresentationData, subject: DeviceAccessMediaLibrarySubject = .wallpaper) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> { return Signal { subscriber in @@ -45,3 +47,76 @@ public func legacyWallpaperPicker(context: AccountContext, presentationData: Pre } } } + +public class LegacyWallpaperItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem { + public var isVideo: Bool { + return false + } + + public var uniqueIdentifier: String! { + return self.asset.localIdentifier + } + + let asset: PHAsset + let screenImage: UIImage + private(set) var thumbnailResource: TelegramMediaResource? + private(set) var imageResource: TelegramMediaResource? + let dimensions: CGSize + + public init(asset: PHAsset, screenImage: UIImage, dimensions: CGSize) { + self.asset = asset + self.screenImage = screenImage + self.dimensions = dimensions + } + + public var originalSize: CGSize { + return self.dimensions + } + + public func thumbnailImageSignal() -> SSignal! { + return SSignal.complete() +// return SSignal(generator: { subscriber -> SDisposable? in +// let disposable = self.thumbnailImage.start(next: { image in +// subscriber.putNext(image) +// subscriber.putCompletion() +// }) +// +// return SBlockDisposable(block: { +// disposable.dispose() +// }) +// }) + } + + public func screenImageSignal(_ position: TimeInterval) -> SSignal! { + return SSignal.single(self.screenImage) + } + + public var originalImage: Signal { + return fetchPhotoLibraryImage(localIdentifier: self.asset.localIdentifier, thumbnail: false) + |> filter { value in + return !(value?.1 ?? true) + } + |> mapToSignal { result -> Signal in + if let result = result { + return .single(result.0) + } else { + return .complete() + } + } + } + + public 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() + }) + }) + } +} diff --git a/submodules/LegacyMediaPickerUI/Sources/Nicegram/NGRoundedVideos.swift b/submodules/LegacyMediaPickerUI/Sources/Nicegram/NGRoundedVideos.swift new file mode 100644 index 00000000000..ff6a11b232a --- /dev/null +++ b/submodules/LegacyMediaPickerUI/Sources/Nicegram/NGRoundedVideos.swift @@ -0,0 +1,32 @@ +import LegacyComponents + +public func canSendAsRoundedVideo( + currentItem: TGMediaPickerGalleryItem?, + editingContext: TGMediaEditingContext?, + selectionContext: TGMediaSelectionContext? +) -> Bool { + guard let editingContext, let selectionContext else { + return false + } + + var hasVideo = false + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if item.isVideo { + hasVideo = true + break + } + } + if let currentItem, currentItem.asset.isVideo { + hasVideo = true + } + + var hasSpoilers = false + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + if editingContext.spoiler(for: item) { + hasSpoilers = true + break + } + } + + return hasVideo && !hasSpoilers +} diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index 30c658bc320..597fd11f87b 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -93,6 +93,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false self.label.isAccessibilityElement = true + self.label.displaysAsynchronously = false super.init() diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 32757f70c3d..b9a2a15a4f8 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -1,9 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -NGDEPS = [ - "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", -] - swift_library( name = "MediaPickerUI", module_name = "MediaPickerUI", @@ -13,7 +9,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = NGDEPS + [ + deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index a2495febd07..b1f9141e160 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -1,3 +1,7 @@ +// MARK: Nicegram RoundedVideos +import NGRoundedVideos +import TooltipUI +// import Foundation import UIKit import Display @@ -268,11 +272,20 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, sendWhenOnlineAvailable = .single(false) } + // MARK: Nicegram RoundedVideos + let canSendAsRoundedVideo = canSendAsRoundedVideo( + currentItem: item, + editingContext: editingContext, + selectionContext: selectionContext + ) + // + 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) + // MARK: Nicegram RoundedVideos, canSendAsRoundedVideo added + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendAsRoundedVideo: canSendAsRoundedVideo, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) let dismissImpl = { [weak model] in model?.dismiss(true, false) dismissAll() @@ -282,6 +295,15 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, dismissImpl() }) } + // MARK: Nicegram RoundedVideos + sheetController.sendAsRoundedVideo = { + NGRoundedVideos.sendAsRoundedVideo = true + + completed(item.asset, false, nil, { + dismissImpl() + }) + } + // sheetController.sendSilently = { [weak model] in model?.interfaceView.onDismiss() @@ -364,5 +386,45 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, } present(legacyController, nil) + // MARK: Nicegram RoundedVideos + Queue.mainQueue().after(1) { [weak model, weak legacyController, weak editingContext, weak selectionContext] in + guard let model, let legacyController else { + return + } + + guard !NGRoundedVideos.sawSendButtonTooltip else { + return + } + + guard canSendAsRoundedVideo( + currentItem: focusItem as? TGMediaPickerGalleryItem, + editingContext: editingContext, + selectionContext: selectionContext + ) else { + return + } + + let tooltipRect = model.interfaceView.doneButtonFrame + + let tooltipScreen = TooltipScreen( + account: context.account, + sharedContext: context.sharedContext, + text: .markdown( + text: NGRoundedVideos.Resources.sendButtonTooltip() + ), + balancedTextLayout: true, + style: .default, + location: .point(tooltipRect, .bottom), + displayDuration: .infinite, + shouldDismissOnTouch: { _, _ in + .dismiss(consume: false) + } + ) + legacyController.present(tooltipScreen, in: .current) + + NGRoundedVideos.sawSendButtonTooltip = true + } + // + return controller } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6018c3a8aae..f4e0444b8ea 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1,5 +1,6 @@ -// MARK: Nicegram InLab -import FeatPartners +// MARK: Nicegram RoundedVideos +import NGRoundedVideos +import TooltipUI // import Foundation import UIKit @@ -226,12 +227,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var requestedMediaAccess = false private var requestedCameraAccess = false - // MARK: Nicegram InLab - private let inLabNode = ASDisplayNode { - InLabButton(location: .galleryAttachment) - } - // - private let containerNode: ASDisplayNode private let backgroundNode: NavigationBackgroundNode fileprivate let gridNode: GridNode @@ -308,7 +303,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.gridNode) self.containerNode.addSubnode(self.scrollingArea) - self.containerNode.addSubnode(self.inLabNode) let selectedCollection = controller.selectedCollection.get() let preloadPromise = self.preloadPromise @@ -1386,33 +1380,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { cameraRect = nil } - // MARK: Nicegram InLab - let inLabSideInset = layout.safeInsets.left + 16 - let inLabFrame = CGRect( - x: inLabSideInset, - y: insets.top, - width: innerBounds.width - inLabSideInset * 2, - height: InLabButton.height - ) - transition.updateFrame( - node: self.inLabNode, - frame: inLabFrame - ) - - var gridFrame = innerBounds - if InLab.showConfig.galleryAttachment { - gridFrame.origin.y = inLabFrame.height + 12 - } - - inLabNode.isHidden = !InLab.showConfig.galleryAttachment - // - let cleanGridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) let gridInsets = UIEdgeInsets(top: insets.top + manageHeight, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) - // MARK: Nicegram InLab, change innerBounds to gridFrame - transition.updateFrame(node: self.gridNode, frame: gridFrame) - // MARK: Nicegram InLab, change innerBounds to gridFrame - self.scrollingArea.frame = gridFrame + transition.updateFrame(node: self.gridNode, frame: innerBounds) + self.scrollingArea.frame = innerBounds transition.updateFrame(node: self.backgroundNode, frame: innerBounds) self.backgroundNode.update(size: bounds.size, transition: transition) @@ -1902,6 +1873,57 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }) } + // MARK: Nicegram RoundedVideos + private func maybeShowRoundedVideoTooltip() { + Queue.mainQueue().after(0.3) { [weak self] in + guard let self else { + return + } + + guard !NGRoundedVideos.sawMoreButtonTooltip else { + return + } + + guard canSendAsRoundedVideo( + currentItem: nil, + editingContext: self.interaction?.editingState, + selectionContext: self.interaction?.selectionState + ) else { + return + } + + guard self.moreButtonNode.iconNode.iconState == .more else { + return + } + + let moreButtonView = self.moreButtonNode.view + let tooltipRect = moreButtonView + .convert( + moreButtonView.bounds, + to: self.view + ) + + let tooltipScreen = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown( + text: NGRoundedVideos.Resources.moreButtonTooltip() + ), + balancedTextLayout: true, + style: .default, + location: .point(tooltipRect, .top), + displayDuration: .infinite, + shouldDismissOnTouch: { _, _ in + .dismiss(consume: false) + } + ) + self.present(tooltipScreen, in: .current) + + NGRoundedVideos.sawMoreButtonTooltip = true + } + } + // + private weak var undoOverlayController: UndoOverlayController? private func showSelectionUndo(item: TGMediaSelectableItem) { let scale = min(2.0, UIScreenScale) @@ -2019,6 +2041,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } } + + // MARK: Nicegram RoundedVideos + maybeShowRoundedVideoTooltip() + // } private func updateThemeAndStrings() { @@ -2050,7 +2076,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.undoOverlayController?.dismissWithCommitAction() } - public func requestDismiss(completion: @escaping () -> Void) { + public func requestDismiss(completion: @escaping () -> Void) { if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 { self.isDismissing = true @@ -2217,6 +2243,25 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {}) }))) } + + // MARK: Nicegram RoundedVideos + if canSendAsRoundedVideo( + currentItem: nil, + editingContext: self?.interaction?.editingState, + selectionContext: self?.interaction?.selectionState + ) { + items.append(.action(ContextMenuActionItem(text: NGRoundedVideos.Resources.buttonTitle(), icon: { theme in + return generateTintedImage(image: NGRoundedVideos.Resources.buttonIcon(), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + NGRoundedVideos.sendAsRoundedVideo = true + + self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, completion: {}) + }))) + } + // + if selectionCount > 1 { if !items.isEmpty { items.append(.separator) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 390142c661e..9ab15c0373c 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -858,10 +858,10 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview - let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: previewText, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: previewText, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) - let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: [], additionalAttributes: nil))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: true) let headerItems: [ListViewItem] = [previewItem, dragItem] diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index d79143102fc..72bd74e0d3b 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -2,6 +2,7 @@ import UIKit #else import AppKit +import TGUIKit #endif import CoreMedia import Accelerate diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 577cb8f2fef..669e6897463 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -3,6 +3,7 @@ import Foundation import UIKit #else import AppKit +import TGUIKit #endif import CoreMedia import SwiftSignalKit diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 8b581bc6b67..72eea9f485d 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -3,6 +3,7 @@ import Foundation import UIKit #else import AppKit +import TGUIKit #endif import SwiftSignalKit import Postbox diff --git a/submodules/MetalEngine/Package.swift b/submodules/MetalEngine/Package.swift new file mode 100644 index 00000000000..ec3838851c4 --- /dev/null +++ b/submodules/MetalEngine/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MetalEngine", + platforms: [.macOS(.v10_13)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "MetalEngine", + targets: ["MetalEngine"]), + ], + dependencies: [ + .package(name: "ShelfPack", path: "../Utils/ShelfPack"), + .package(name: "TGUIKit", path: "../../../../packages/TGUIKit"), + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "MetalEngine", + dependencies: [.product(name: "ShelfPack", package: "ShelfPack", condition: nil), + .product(name: "TGUIKit", package: "TGUIKit", condition: nil)], + path: "Sources/"), + ] +) diff --git a/submodules/MetalEngine/Sources/MetalEngine.swift b/submodules/MetalEngine/Sources/MetalEngine.swift index 39356c93196..87672cf6b51 100644 --- a/submodules/MetalEngine/Sources/MetalEngine.swift +++ b/submodules/MetalEngine/Sources/MetalEngine.swift @@ -1,8 +1,16 @@ import Foundation import Metal + +#if os(iOS) +import Display import UIKit +#else +import AppKit +import TGUIKit +#endif + + import IOSurface -import Display import ShelfPack public final class Placeholder { @@ -658,6 +666,17 @@ public final class MetalEngine { fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:] init?(device: MTLDevice) { + + self.device = device + + guard let commandQueue = device.makeCommandQueue() else { + return nil + } + self.commandQueue = commandQueue + + let library: MTLLibrary? + + #if os(iOS) let mainBundle = Bundle(for: Impl.self) guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { return nil @@ -665,23 +684,32 @@ public final class MetalEngine { guard let bundle = Bundle(path: path) else { return nil } - - self.device = device - - guard let commandQueue = device.makeCommandQueue() else { + library = try? device.makeDefaultLibrary(bundle: bundle) + #else + let mainBundle = Bundle(for: Impl.self) + guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { return nil } - self.commandQueue = commandQueue + guard let bundle = Bundle(path: path) else { + return nil + } + guard let path = bundle.path(forResource: "MetalEngineShaders", ofType: "metallib") else { + return nil + } + library = try? device.makeLibrary(URL: .init(fileURLWithPath: path)) + #endif + + - guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { + guard let lib = library else { return nil } - self.library = library + self.library = lib - guard let vertexFunction = library.makeFunction(name: "clearVertex") else { + guard let vertexFunction = lib.makeFunction(name: "clearVertex") else { return nil } - guard let fragmentFunction = library.makeFunction(name: "clearFragment") else { + guard let fragmentFunction = lib.makeFunction(name: "clearFragment") else { return nil } diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 9e184bca04a..3ea2d1aa772 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -166,6 +166,7 @@ public enum PeerInfoAvatarListItem: Equatable { } } + var videoRepresentations: [VideoRepresentationWithReference] { switch self { case .custom: @@ -602,7 +603,12 @@ private final class VariableBlurView: UIVisualEffectView { fatalError("init(coder:) has not been implemented") } - override func updateTraitsIfNeeded() { + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + if #available(iOS 13.0, *) { + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + self.resetEffect() + } + } } private func resetEffect() { diff --git a/submodules/Postbox/Sources/CachedPeerData.swift b/submodules/Postbox/Sources/CachedPeerData.swift index 582d4dcdc61..757348d1368 100644 --- a/submodules/Postbox/Sources/CachedPeerData.swift +++ b/submodules/Postbox/Sources/CachedPeerData.swift @@ -1,5 +1,5 @@ -public protocol CachedPeerData: PostboxCoding { +public protocol CachedPeerData: AnyObject, PostboxCoding { var peerIds: Set { get } var messageIds: Set { get } diff --git a/submodules/Postbox/Sources/MessageHistoryViewState.swift b/submodules/Postbox/Sources/MessageHistoryViewState.swift index b2e937db9ec..58700fd3b3f 100644 --- a/submodules/Postbox/Sources/MessageHistoryViewState.swift +++ b/submodules/Postbox/Sources/MessageHistoryViewState.swift @@ -1534,9 +1534,25 @@ final class HistoryViewLoadedState { author = updatedAuthor rebuild = true } + var associatedMessages = message.associatedMessages + for (id, associatedMessage) in message.associatedMessages { + var peers = associatedMessage.peers + var author = associatedMessage.author + for (peerId, _) in associatedMessage.peers { + if let updatedPeer = updatedPeers[peerId] { + peers[peerId] = updatedPeer + rebuild = true + } + } + if let authorValue = author, let updatedAuthor = updatedPeers[authorValue.id] { + author = updatedAuthor + rebuild = true + } + associatedMessages[id] = associatedMessage.withUpdatedPeers(peers).withUpdatedAuthor(author) + } if rebuild { - let updatedMessage = message.withUpdatedPeers(peers).withUpdatedAuthor(author) + let updatedMessage = message.withUpdatedPeers(peers).withUpdatedAuthor(author).withUpdatedAssociatedMessages(associatedMessages) return .MessageEntry(MessageHistoryMessageEntry(message: updatedMessage, location: value.location, monthLocation: value.monthLocation, attributes: value.attributes), reloadAssociatedMessages: reloadAssociatedMessages, reloadPeers: reloadPeers) } case .IntermediateMessageEntry: diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 77b604e786d..0b2233d3817 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -116,6 +116,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/InvisibleInkDustNode", "//submodules/AlertUI", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index a54f4aeac03..e4d93d125e8 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -30,10 +30,11 @@ private final class CreateGiveawayControllerArguments { let openCountriesSelection: () -> Void let openPremiumIntro: () -> Void let scrollToDate: () -> Void + let scrollToDescription: () -> Void let setItemIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void let removeChannel: (EnginePeer.Id) -> Void - init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openCountriesSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, scrollToDate: @escaping () -> Void, setItemIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeChannel: @escaping (EnginePeer.Id) -> Void) { + init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openCountriesSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, scrollToDate: @escaping () -> Void, scrollToDescription: @escaping () -> Void, setItemIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeChannel: @escaping (EnginePeer.Id) -> Void) { self.context = context self.updateState = updateState self.dismissInput = dismissInput @@ -42,6 +43,7 @@ private final class CreateGiveawayControllerArguments { self.openCountriesSelection = openCountriesSelection self.openPremiumIntro = openPremiumIntro self.scrollToDate = scrollToDate + self.scrollToDescription = scrollToDescription self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions self.removeChannel = removeChannel } @@ -53,11 +55,14 @@ private enum CreateGiveawaySection: Int32 { case subscriptions case channels case users + case winners + case prizeDescription case time case duration } private enum CreateGiveawayEntryTag: ItemListItemTag { + case description case date func isEqual(to other: ItemListItemTag) -> Bool { @@ -92,14 +97,21 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { case usersNew(PresentationTheme, String, String, Bool) case usersInfo(PresentationTheme, String) + case durationHeader(PresentationTheme, String) + case duration(Int32, PresentationTheme, Int32, String, String, String, String?, Bool) + case durationInfo(PresentationTheme, String) + + case prizeDescription(PresentationTheme, String, Bool) + case prizeDescriptionText(PresentationTheme, String, String, Int32) + case prizeDescriptionInfo(PresentationTheme, String) + case timeHeader(PresentationTheme, String) case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Int32?, Int32?, Bool, Bool) case timeInfo(PresentationTheme, String) - case durationHeader(PresentationTheme, String) - case duration(Int32, PresentationTheme, Int32, String, String, String, String?, Bool) - case durationInfo(PresentationTheme, String) + case winners(PresentationTheme, String, Bool) + case winnersInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -113,61 +125,75 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { return CreateGiveawaySection.channels.rawValue case .usersHeader, .usersAll, .usersNew, .usersInfo: return CreateGiveawaySection.users.rawValue - case .timeHeader, .timeExpiryDate, .timeCustomPicker, .timeInfo: - return CreateGiveawaySection.time.rawValue case .durationHeader, .duration, .durationInfo: return CreateGiveawaySection.duration.rawValue + case .prizeDescription, .prizeDescriptionText, .prizeDescriptionInfo: + return CreateGiveawaySection.prizeDescription.rawValue + case .timeHeader, .timeExpiryDate, .timeCustomPicker, .timeInfo: + return CreateGiveawaySection.time.rawValue + case .winners, .winnersInfo: + return CreateGiveawaySection.winners.rawValue } } var stableId: Int32 { switch self { - case .header: - return -1 - case .createGiveaway: - return 0 - case .awardUsers: - return 1 - case .prepaidHeader: - return 2 - case .prepaid: - return 3 - case .subscriptionsHeader: - return 4 - case .subscriptions: - return 5 - case .subscriptionsInfo: - return 6 - case .channelsHeader: - return 7 - case let .channel(index, _, _, _, _): - return 8 + index - case .channelAdd: - return 100 - case .channelsInfo: - return 101 - case .usersHeader: - return 102 - case .usersAll: - return 103 - case .usersNew: - return 104 - case .usersInfo: - return 105 - case .timeHeader: - return 106 - case .timeExpiryDate: - return 107 - case .timeCustomPicker: - return 108 - case .timeInfo: - return 109 - case .durationHeader: - return 110 - case let .duration(index, _, _, _, _, _, _, _): - return 111 + index - case .durationInfo: - return 120 + case .header: + return -1 + case .createGiveaway: + return 0 + case .awardUsers: + return 1 + case .prepaidHeader: + return 2 + case .prepaid: + return 3 + case .subscriptionsHeader: + return 4 + case .subscriptions: + return 5 + case .subscriptionsInfo: + return 6 + case .channelsHeader: + return 7 + case let .channel(index, _, _, _, _): + return 8 + index + case .channelAdd: + return 100 + case .channelsInfo: + return 101 + case .usersHeader: + return 102 + case .usersAll: + return 103 + case .usersNew: + return 104 + case .usersInfo: + return 105 + case .durationHeader: + return 106 + case let .duration(index, _, _, _, _, _, _, _): + return 107 + index + case .durationInfo: + return 200 + case .prizeDescription: + return 201 + case .prizeDescriptionText: + return 202 + case .prizeDescriptionInfo: + return 203 + case .winners: + return 204 + case .winnersInfo: + return 205 + case .timeHeader: + return 206 + case .timeExpiryDate: + return 207 + case .timeCustomPicker: + return 208 + case .timeInfo: + return 209 } } @@ -269,7 +295,42 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } else { return false } - + case let .durationHeader(lhsTheme, lhsText): + if case let .durationHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .duration(lhsIndex, lhsTheme, lhsMonths, lhsTitle, lhsSubtitle, lhsLabel, lhsBadge, lhsIsSelected): + if case let .duration(rhsIndex, rhsTheme, rhsMonths, rhsTitle, rhsSubtitle, rhsLabel, rhsBadge, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsMonths == rhsMonths, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel, lhsBadge == rhsBadge, lhsIsSelected == rhsIsSelected { + return true + } else { + return false + } + case let .durationInfo(lhsTheme, lhsText): + if case let .durationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .prizeDescription(lhsTheme, lhsText, lhsValue): + if case let .prizeDescription(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .prizeDescriptionText(lhsTheme, lhsPlaceholder, lhsValue, lhsCount): + if case let .prizeDescriptionText(rhsTheme, rhsPlaceholder, rhsValue, rhsCount) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue, lhsCount == rhsCount { + return true + } else { + return false + } + case let .prizeDescriptionInfo(lhsTheme, lhsText): + if case let .prizeDescriptionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .timeHeader(lhsTheme, lhsText): if case let .timeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -294,20 +355,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { } else { return false } - case let .durationHeader(lhsTheme, lhsText): - if case let .durationHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .winners(lhsTheme, lhsText, lhsValue): + if case let .winners(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .duration(lhsIndex, lhsTheme, lhsMonths, lhsTitle, lhsSubtitle, lhsLabel, lhsBadge, lhsIsSelected): - if case let .duration(rhsIndex, rhsTheme, rhsMonths, rhsTitle, rhsSubtitle, rhsLabel, rhsBadge, rhsIsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsMonths == rhsMonths, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel, lhsBadge == rhsBadge, lhsIsSelected == rhsIsSelected { - return true - } else { - return false - } - case let .durationInfo(lhsTheme, lhsText): - if case let .durationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .winnersInfo(lhsTheme, lhsText): + if case let .winnersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -423,6 +478,46 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { }) case let .usersInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .durationHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .duration(_, _, months, title, subtitle, label, badge, isSelected): + return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleFont: .small, label: .generic(label), badge: badge, isSelected: isSelected, sectionId: self.section, action: { + arguments.updateState { state in + var updatedState = state + updatedState.selectedMonths = months + return updatedState + } + }) + case let .durationInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in + arguments.openPremiumIntro() + }) + case let .prizeDescription(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.showPrizeDescription = value + return updatedState + } + }) + case let .prizeDescriptionText(_, placeholder, value, count): + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.prizeDescription = value + return updatedState + } + }, updatedFocus: { focused in + if focused { + Queue.mainQueue().after(0.05) { + arguments.scrollToDescription() + } + } + }, action: { + arguments.dismissInput() + }) + case let .prizeDescriptionInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .timeHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .timeExpiryDate(theme, dateTimeFormat, value, active): @@ -492,20 +587,16 @@ private enum CreateGiveawayEntry: ItemListNodeEntry { }, tag: CreateGiveawayEntryTag.date) case let .timeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .durationHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .duration(_, _, months, title, subtitle, label, badge, isSelected): - return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleFont: .small, label: .generic(label), badge: badge, isSelected: isSelected, sectionId: self.section, action: { + case let .winners(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state - updatedState.selectedMonths = months + updatedState.showWinners = value return updatedState } }) - case let .durationInfo(_, text): - return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in - arguments.openPremiumIntro() - }) + case let .winnersInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -573,7 +664,55 @@ private func createGiveawayControllerEntries( entries.append(.prepaid(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayCount(prepaidGiveaway.quantity), presentationData.strings.BoostGift_PrepaidGiveawayMonths("\(prepaidGiveaway.months)").string, prepaidGiveaway)) } - if case .giveaway = state.mode { + let appendDurationEntries = { + entries.append(.durationHeader(presentationData.theme, presentationData.strings.BoostGift_DurationTitle.uppercased())) + + let recipientCount: Int + switch state.mode { + case .giveaway: + recipientCount = Int(state.subscriptions) + case .gift: + recipientCount = state.peers.count + } + + var i: Int32 = 0 + var existingMonths = Set() + for product in products { + if existingMonths.contains(product.months) { + continue + } + existingMonths.insert(product.months) + let giftTitle: String + if product.months == 12 { + giftTitle = presentationData.strings.Premium_Gift_Years(1) + } else { + giftTitle = presentationData.strings.Premium_Gift_Months(product.months) + } + + let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(defaultPrice.0)) * 100.0) + let discount: String? + if discountValue > 0 { + discount = "-\(discountValue)%" + } else { + discount = nil + } + + let subtitle = "\(product.storeProduct.price) x \(recipientCount)" + let label = product.storeProduct.multipliedPrice(count: recipientCount) + + let selectedMonths = state.selectedMonths ?? 12 + let isSelected = product.months == selectedMonths + + entries.append(.duration(i, presentationData.theme, product.months, giftTitle, subtitle, label, discount, isSelected)) + + i += 1 + } + + entries.append(.durationInfo(presentationData.theme, presentationData.strings.BoostGift_PremiumInfo)) + } + + switch state.mode { + case .giveaway: if case .generic = subject { entries.append(.subscriptionsHeader(presentationData.theme, presentationData.strings.BoostGift_QuantityTitle.uppercased(), presentationData.strings.BoostGift_QuantityBoosts(state.subscriptions * 4))) entries.append(.subscriptions(presentationData.theme, state.subscriptions)) @@ -617,56 +756,35 @@ private func createGiveawayControllerEntries( entries.append(.usersNew(presentationData.theme, presentationData.strings.BoostGift_OnlyNewSubscribers, countriesText, state.onlyNewEligible)) entries.append(.usersInfo(presentationData.theme, presentationData.strings.BoostGift_LimitSubscribersInfo)) - entries.append(.timeHeader(presentationData.theme, presentationData.strings.BoostGift_DateTitle.uppercased())) - entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate, state.pickingExpiryDate, state.pickingExpiryTime)) - entries.append(.timeInfo(presentationData.theme, presentationData.strings.BoostGift_DateInfo(presentationData.strings.BoostGift_DateInfoSubscribers(Int32(state.subscriptions))).string)) - } - - if case .generic = subject { - entries.append(.durationHeader(presentationData.theme, presentationData.strings.BoostGift_DurationTitle.uppercased())) - - let recipientCount: Int - switch state.mode { - case .giveaway: - recipientCount = Int(state.subscriptions) - case .gift: - recipientCount = state.peers.count + if case .generic = subject { + appendDurationEntries() } - var i: Int32 = 0 - var existingMonths = Set() - for product in products { - if existingMonths.contains(product.months) { - continue - } - existingMonths.insert(product.months) - let giftTitle: String - if product.months == 12 { - giftTitle = presentationData.strings.Premium_Gift_Years(1) - } else { - giftTitle = presentationData.strings.Premium_Gift_Months(product.months) - } - - let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(defaultPrice.0)) * 100.0) - let discount: String? - if discountValue > 0 { - discount = "-\(discountValue)%" + entries.append(.prizeDescription(presentationData.theme, presentationData.strings.BoostGift_AdditionalPrizes, state.showPrizeDescription)) + var prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoOff + if state.showPrizeDescription { + entries.append(.prizeDescriptionText(presentationData.theme, presentationData.strings.BoostGift_AdditionalPrizesPlaceholder, state.prizeDescription, state.subscriptions)) + + let monthsString = presentationData.strings.BoostGift_AdditionalPrizesInfoForMonths(state.selectedMonths ?? 3) + if state.prizeDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + let subscriptionsString = presentationData.strings.BoostGift_AdditionalPrizesInfoSubscriptions(state.subscriptions).replacingOccurrences(of: "\(state.subscriptions) ", with: "") + prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoOn("\(state.subscriptions)", subscriptionsString, monthsString).string } else { - discount = nil + let subscriptionsString = presentationData.strings.BoostGift_AdditionalPrizesInfoWithSubscriptions(state.subscriptions).replacingOccurrences(of: "\(state.subscriptions) ", with: "") + let description = "\(state.prizeDescription) \(subscriptionsString)" + prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoOn("\(state.subscriptions)", description, monthsString).string } - - let subtitle = "\(product.storeProduct.price) x \(recipientCount)" - let label = product.storeProduct.multipliedPrice(count: recipientCount) - - let selectedMonths = state.selectedMonths ?? 12 - let isSelected = product.months == selectedMonths - - entries.append(.duration(i, presentationData.theme, product.months, giftTitle, subtitle, label, discount, isSelected)) - - i += 1 } + entries.append(.prizeDescriptionInfo(presentationData.theme, prizeDescriptionInfoText)) - entries.append(.durationInfo(presentationData.theme, presentationData.strings.BoostGift_PremiumInfo)) + entries.append(.winners(presentationData.theme, presentationData.strings.BoostGift_Winners, state.showWinners)) + entries.append(.winnersInfo(presentationData.theme, presentationData.strings.BoostGift_WinnersInfo)) + + entries.append(.timeHeader(presentationData.theme, presentationData.strings.BoostGift_DateTitle.uppercased())) + entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate, state.pickingExpiryDate, state.pickingExpiryTime)) + entries.append(.timeInfo(presentationData.theme, presentationData.strings.BoostGift_DateInfo(presentationData.strings.BoostGift_DateInfoSubscribers(Int32(state.subscriptions))).string)) + case .gift: + appendDurationEntries() } return entries @@ -680,11 +798,14 @@ private struct CreateGiveawayControllerState: Equatable { var mode: Mode var subscriptions: Int32 - var channels: [EnginePeer.Id] - var peers: [EnginePeer.Id] + var channels: [EnginePeer.Id] = [] + var peers: [EnginePeer.Id] = [] var selectedMonths: Int32? - var countries: [String] - var onlyNewEligible: Bool + var countries: [String] = [] + var onlyNewEligible: Bool = false + var showWinners: Bool = true + var showPrizeDescription: Bool = false + var prizeDescription: String = "" var time: Int32 var pickingExpiryTime = false var pickingExpiryDate = false @@ -722,7 +843,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio let minDate = currentTime + 60 * 30 let maxDate = currentTime + context.userLimits.maxGiveawayPeriodSeconds - let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], countries: [], onlyNewEligible: false, time: expiryTime) + let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, time: expiryTime) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -739,6 +860,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio var openPremiumIntroImpl: (() -> Void)? var presentControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? + var scrollToDescriptionImpl: (() -> Void)? var scrollToDateImpl: (() -> Void)? var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? @@ -757,6 +879,8 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio openPremiumIntroImpl?() }, scrollToDate: { scrollToDateImpl?() + }, scrollToDescription: { + scrollToDescriptionImpl?() }, setItemIdWithRevealedOptions: { itemId, fromItemId in updateState { state in var updatedState = state @@ -865,6 +989,9 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio if previousState.channels.count > state.channels.count { animateChanges = true } + if previousState.showPrizeDescription != state.showPrizeDescription { + animateChanges = true + } } var peers: [EnginePeer.Id: EnginePeer] = [:] @@ -886,7 +1013,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.beganInteractiveDragging = { - dismissInputImpl?() +// dismissInputImpl?() } presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) @@ -948,7 +1075,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio let quantity: Int32 switch state.mode { case .giveaway: - purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) + purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, showWinners: state.showWinners, prizeDescription: state.prizeDescription.isEmpty ? nil : state.prizeDescription, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) quantity = selectedProduct.giftOption.storeQuantity case .gift: purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) @@ -1040,7 +1167,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio return updatedState } - let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time) + let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, showWinners: state.showWinners, prizeDescription: state.prizeDescription.isEmpty ? nil : state.prizeDescription, randomId: Int64.random(in: .min ..< .max), untilDate: state.time) |> deliverOnMainQueue).startStandalone(completed: { if let controller, let navigationController = controller.navigationController as? NavigationController { var controllers = navigationController.viewControllers @@ -1165,6 +1292,28 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio pushControllerImpl?(controller) } + scrollToDescriptionImpl = { [weak controller] in + controller?.afterLayout({ + guard let controller = controller else { + return + } + + var resultItemNode: ListViewItemNode? + let _ = controller.frameForItemNode({ listItemNode in + if let itemNode = listItemNode as? ItemListItemNode { + if let tag = itemNode.tag as? CreateGiveawayEntryTag, tag == .description { + resultItemNode = listItemNode + return true + } + } + return false + }) + if let resultItemNode = resultItemNode { + controller.ensureItemNodeVisible(resultItemNode, overflow: 120.0, atTop: false) + } + }) + } + scrollToDateImpl = { [weak controller] in controller?.afterLayout({ guard let controller = controller else { diff --git a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift index 19f18297f72..291d4d357da 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayFooterItem.swift @@ -97,8 +97,8 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode { if let inputHeight = layout.inputHeight, inputHeight > 0.0 { totalPanelHeight = panelHeight + insets.bottom } else { + totalPanelHeight = panelHeight + insets.bottom panelHeight += insets.bottom - totalPanelHeight = panelHeight } let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) @@ -155,7 +155,7 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) - return panelHeight + return totalPanelHeight } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift index 094a06bee50..e3fd75b9970 100644 --- a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -10,24 +10,29 @@ import LegacyComponents import AvatarNode import AccountContext import TelegramCore +import MergedAvatarsNode +import MultilineTextComponent +import TelegramPresentationData private let sceneVersion: Int = 3 class GiftAvatarComponent: Component { let context: AccountContext - let peer: EnginePeer? + let theme: PresentationTheme + let peers: [EnginePeer] let isVisible: Bool let hasIdleAnimations: Bool - init(context: AccountContext, peer: EnginePeer?, isVisible: Bool, hasIdleAnimations: Bool) { + init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool) { self.context = context - self.peer = peer + self.theme = theme + self.peers = peers self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations } static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peer == rhs.peer && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations + return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations } final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -52,6 +57,10 @@ class GiftAvatarComponent: Component { private let sceneView: SCNView private let avatarNode: ImageNode + private var mergedAvatarsNode: MergedAvatarsNode? + + private let badgeBackground = ComponentView() + private let badge = ComponentView() private var previousInteractionTimestamp: Double = 0.0 private var timer: SwiftSignalKit.Timer? @@ -242,11 +251,75 @@ class GiftAvatarComponent: Component { } self.hasIdleAnimations = component.hasIdleAnimations - let avatarSize = CGSize(width: 100.0, height: 100.0) - if let peer = component.peer { - self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) + + if component.peers.count > 1 { + let avatarSize = CGSize(width: 60.0, height: 60.0) + + let mergedAvatarsNode: MergedAvatarsNode + if let current = self.mergedAvatarsNode { + mergedAvatarsNode = current + } else { + mergedAvatarsNode = MergedAvatarsNode() + mergedAvatarsNode.isUserInteractionEnabled = false + self.addSubview(mergedAvatarsNode.view) + self.mergedAvatarsNode = mergedAvatarsNode + } + + mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.map { $0._asPeer() }.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0) + let avatarsSize = CGSize(width: avatarSize.width + 30.0 * CGFloat(min(3, component.peers.count) - 1), height: avatarSize.height) + mergedAvatarsNode.updateLayout(size: avatarsSize) + mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarsSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarsSize) + self.avatarNode.isHidden = true + } else { + self.mergedAvatarsNode?.view.removeFromSuperview() + self.mergedAvatarsNode = nil + self.avatarNode.isHidden = false + + let avatarSize = CGSize(width: 100.0, height: 100.0) + if let peer = component.peers.first { + self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) + } + self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarSize) + } + + if component.peers.count > 3 { + let badgeTextSize = self.badge.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "+\(component.peers.count - 3)", font: Font.with(size: 10.0, design: .round, weight: .semibold), textColor: component.theme.list.itemCheckColors.foregroundColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let lineWidth = 1.0 + UIScreenPixel + let badgeSize = CGSize(width: max(17.0, badgeTextSize.width + 7.0) + lineWidth * 2.0, height: 17.0 + lineWidth * 2.0) + let _ = self.badgeBackground.update( + transition: .immediate, + component: AnyComponent( + RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: badgeSize.height / 2.0, stroke: lineWidth, strokeColor: component.theme.list.blocksBackgroundColor) + ), + environment: {}, + containerSize: badgeSize + ) + + if let badgeTextView = self.badge.view, let badgeBackgroundView = self.badgeBackground.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + + let avatarsSize = CGSize(width: 120.0, height: 60.0) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width + avatarsSize.width) / 2.0) - 19.0 - lineWidth, y: 113.0 + avatarsSize.height / 2.0 - 15.0 - lineWidth), size: badgeSize) + badgeBackgroundView.frame = backgroundFrame + badgeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeTextSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - badgeTextSize.height / 2.0) - UIScreenPixel), size: badgeTextSize) + } + } else { + self.badge.view?.removeFromSuperview() + self.badgeBackground.view?.removeFromSuperview() } - self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize) return availableSize } diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift index 390a02f78b4..fc605e24627 100644 --- a/submodules/PremiumUI/Sources/GiveawayInfoController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -31,19 +31,70 @@ public func presentGiveawayInfoController( guard let message else { return } - guard let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else { - return + + let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway + let giveawayResults = message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults + + var channelPeerId: EnginePeer.Id? + if let giveaway { + if let peerId = giveaway.channelPeerIds.first { + channelPeerId = peerId + } + } else if let _ = giveawayResults { + channelPeerId = message.author?.id + } + + var quantity: Int32 = 0 + if let giveaway { + quantity = giveaway.quantity + } else if let giveawayResults { + quantity = giveawayResults.winnersCount + giveawayResults.unclaimedCount + } + + var months: Int32 = 0 + if let giveaway { + months = giveaway.months + } else if let giveawayResults { + months = giveawayResults.months + } + + var prizeDescription: String? + if let giveaway { + prizeDescription = giveaway.prizeDescription + } else if let giveawayResults { + prizeDescription = giveawayResults.prizeDescription + } + + var untilDateValue: Int32 = 0 + if let giveaway { + untilDateValue = giveaway.untilDate + } else if let giveawayResults { + untilDateValue = giveawayResults.untilDate + } + + var onlyNewSubscribers = false + if let giveaway, giveaway.flags.contains(.onlyNewSubscribers) { + onlyNewSubscribers = true + } else if let giveawayResults, giveawayResults.flags.contains(.onlyNewSubscribers) { + onlyNewSubscribers = true + } + + var channelsCount: Int32 = 1 + if let giveaway { + channelsCount = Int32(giveaway.channelPeerIds.count) + } else if let giveawayResults { + channelsCount = 1 + giveawayResults.additionalChannelsCount } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var peerName = "" - if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] { + if let peerId = channelPeerId, let peer = message.peers[peerId] { peerName = EnginePeer(peer).compactDisplayTitle } let timeZone = TimeZone.current - let untilDate = stringForDate(timestamp: giveaway.untilDate, timeZone: timeZone, strings: presentationData.strings) + let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings) let title: String let text: String @@ -55,6 +106,11 @@ public func presentGiveawayInfoController( dismissImpl?() })] + var additionalPrizes = "" + if let prizeDescription, !prizeDescription.isEmpty { + additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(quantity) \(prizeDescription)").string + } + switch giveawayInfo { case let .ongoing(start, status): let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( @@ -66,23 +122,23 @@ public func presentGiveawayInfoController( let intro: String if case .almostOver = status { - intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string } else { - intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string } let ending: String - if giveaway.flags.contains(.onlyNewSubscribers) { - let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { - ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), startDate).string + if onlyNewSubscribers { + let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity) + if channelsCount > 1 { + ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), startDate).string } else { ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string } } else { - let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { - ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string + let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity) + if channelsCount > 1 { + ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string } else { ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string } @@ -91,8 +147,8 @@ public func presentGiveawayInfoController( var participation: String switch status { case .notQualified: - if giveaway.channelPeerIds.count > 1 { - participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), untilDate).string + if channelsCount > 1 { + participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), untilDate).string } else { participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string } @@ -111,8 +167,8 @@ public func presentGiveawayInfoController( participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry } case .participating: - if giveaway.channelPeerIds.count > 1 { - participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string + if channelsCount > 1 { + participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string } else { participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string } @@ -124,7 +180,7 @@ public func presentGiveawayInfoController( participation = "\n\n\(participation)" } - text = "\(intro)\n\n\(ending)\(participation)" + text = "\(intro)\(additionalPrizes)\n\n\(ending)\(participation)" case let .finished(status, start, finish, _, activatedCount): let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat), @@ -134,19 +190,19 @@ public func presentGiveawayInfoController( let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings) title = presentationData.strings.Chat_Giveaway_Info_EndedTitle - let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string var ending: String - if giveaway.flags.contains(.onlyNewSubscribers) { - let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { + if onlyNewSubscribers { + let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity) + if channelsCount > 1 { ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string } else { ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string } } else { - let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { + let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity) + if channelsCount > 1 { ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string } else { ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string @@ -156,7 +212,7 @@ public func presentGiveawayInfoController( if activatedCount > 0 { ending += " " + presentationData.strings.Chat_Giveaway_Info_ActivatedLinks(activatedCount) } - + var result: String switch status { case .refunded: @@ -166,9 +222,9 @@ public func presentGiveawayInfoController( dismissImpl?() })] case .notWon: - result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_DidntWin + result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n" case let .won(slug): - result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_Won("🏆").string + result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n" actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: { dismissImpl?() openLink(slug) @@ -177,7 +233,7 @@ public func presentGiveawayInfoController( })] } - text = "\(intro)\n\n\(ending)\(result)" + text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)" } let alertController = giveawayInfoAlertController( diff --git a/submodules/PremiumUI/Sources/LimitsPageComponent.swift b/submodules/PremiumUI/Sources/LimitsPageComponent.swift index 55c909ca8a6..709e075f86f 100644 --- a/submodules/PremiumUI/Sources/LimitsPageComponent.swift +++ b/submodules/PremiumUI/Sources/LimitsPageComponent.swift @@ -358,17 +358,17 @@ private final class LimitsListComponent: CombinedComponent { let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let colors = [ - UIColor(rgb: 0x5ba0ff), - UIColor(rgb: 0x798aff), - UIColor(rgb: 0x9377ff), - UIColor(rgb: 0xac64f3), - UIColor(rgb: 0xc456ae), - UIColor(rgb: 0xcf579a), - UIColor(rgb: 0xdb5887), - UIColor(rgb: 0xdb496f), - UIColor(rgb: 0xe95d44), - UIColor(rgb: 0xf2822a), - UIColor(rgb: 0xfdb529) + UIColor(rgb: 0xdf7138), + UIColor(rgb: 0xd6593f), + UIColor(rgb: 0xca4550), + UIColor(rgb: 0xbc496d), + UIColor(rgb: 0xae4c92), + UIColor(rgb: 0x9153e5), + UIColor(rgb: 0x825af6), + UIColor(rgb: 0x676bf7), + UIColor(rgb: 0x5991f8), + UIColor(rgb: 0x5b99d0), + UIColor(rgb: 0x5ea4a4) ] let items: [AnyComponentWithIdentity] = Limit.allCases.enumerated().map { index, value in diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift new file mode 100644 index 00000000000..8425a7586ad --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -0,0 +1,1657 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import Markdown +import TextFormat +import SolidRoundedButtonComponent +import BlurredBackgroundComponent +import UndoUI + +func requiredBoostSubjectLevel(subject: BoostSubject, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { + switch subject { + case .stories: + return 1 + case let .channelReactions(reactionCount): + return reactionCount + case let .nameColors(colors): + if let value = context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[colors.rawValue] { + return value + } else { + return 1 + } + case .nameIcon: + return configuration.minChannelNameIconLevel + case .profileColors: + return configuration.minChannelProfileColorLevel + case .profileIcon: + return configuration.minChannelProfileIconLevel + case .emojiStatus: + return configuration.minChannelEmojiStatusLevel + case .wallpaper: + return configuration.minChannelWallpaperLevel + case .customWallpaper: + return configuration.minChannelCustomWallpaperLevel + } +} + +public enum BoostSubject: Equatable { + case stories + case channelReactions(reactionCount: Int32) + case nameColors(colors: PeerNameColor) + case nameIcon + case profileColors + case profileIcon + case emojiStatus + case wallpaper + case customWallpaper + + public func requiredLevel(context: AccountContext, configuration: PremiumConfiguration) -> Int32 { + return requiredBoostSubjectLevel(subject: self, context: context, configuration: configuration) + } +} + +private final class LevelHeaderComponent: CombinedComponent { + let theme: PresentationTheme + let text: String + + init( + theme: PresentationTheme, + text: String + ) { + self.theme = theme + self.text = text + } + + static func ==(lhs: LevelHeaderComponent, rhs: LevelHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let text = Child(MultilineTextComponent.self) + let leftLine = Child(Rectangle.self) + let rightLine = Child(Rectangle.self) + + return { context in + let component = context.component + + let outerInset: CGFloat = 28.0 + let innerInset: CGFloat = 9.0 + + let height: CGFloat = 50.0 + let backgroundHeight: CGFloat = 34.0 + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.semibold(15.0), textColor: .white)), + horizontalAlignment: .center + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let backgroundWidth: CGFloat = floor(text.size.width + 21.0) + + let background = background.update( + component: RoundedRectangle(colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], cornerRadius: backgroundHeight / 2.0, gradientDirection: .horizontal), + availableSize: CGSize(width: backgroundWidth, height: backgroundHeight), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) + ) + + let remainingWidth = (context.availableSize.width - background.size.width) / 2.0 + let lineSize = remainingWidth - outerInset - innerInset + let lineWidth = 1.0 - UIScreenPixel + + let leftLine = leftLine.update( + component: Rectangle( + color: component.theme.actionSheet.secondaryTextColor.withMultipliedAlpha(0.5) + ), + availableSize: CGSize(width: lineSize, height: lineWidth), + transition: .immediate + ) + context.add(leftLine + .position(CGPoint(x: outerInset + lineSize / 2.0, y: height / 2.0)) + ) + + let rightLine = rightLine.update( + component: Rectangle( + color: component.theme.actionSheet.secondaryTextColor.withMultipliedAlpha(0.5) + ), + availableSize: CGSize(width: lineSize, height: lineWidth), + transition: .immediate + ) + context.add(rightLine + .position(CGPoint(x: context.availableSize.width - outerInset - lineSize / 2.0, y: height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: height) + } + } +} + +private final class LevelPerkComponent: CombinedComponent { + let theme: PresentationTheme + let iconName: String + let text: String + + init( + theme: PresentationTheme, + iconName: String, + text: String + ) { + self.theme = theme + self.iconName = iconName + self.text = text + } + + static func ==(lhs: LevelPerkComponent, rhs: LevelPerkComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + static var body: Body { + let icon = Child(BundleIconComponent.self) + let text = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + let outerInset: CGFloat = 28.0 + let height: CGFloat = 44.0 + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.theme.actionSheet.controlAccentColor + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(icon + .position(CGPoint(x: outerInset + icon.size.width / 2.0, y: height / 2.0)) + ) + + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.semibold(15.0), textColor: component.theme.actionSheet.primaryTextColor)), + horizontalAlignment: .center + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: outerInset * 2.0 + 18.0 + text.size.width / 2.0, y: height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: height) + } + } +} + +private final class LevelSectionComponent: CombinedComponent { + enum Perk: Equatable { + case story(Int32) + case reaction(Int32) + case nameColor(Int32) + case profileColor(Int32) + case profileIcon + case linkColor(Int32) + case linkIcon + case emojiStatus + case wallpaper(Int32) + case customWallpaper + + func title(strings: PresentationStrings) -> String { + switch self { + case let .story(value): + return strings.ChannelBoost_Table_StoriesPerDay(value) + case let .reaction(value): + return strings.ChannelBoost_Table_CustomReactions(value) + case let .nameColor(value): + return strings.ChannelBoost_Table_NameColor(value) + case let .profileColor(value): + return strings.ChannelBoost_Table_ProfileColor(value) + case .profileIcon: + return strings.ChannelBoost_Table_ProfileLogo + case let .linkColor(value): + return strings.ChannelBoost_Table_StyleForHeaders(value) + case .linkIcon: + return strings.ChannelBoost_Table_HeadersLogo + case .emojiStatus: + return strings.ChannelBoost_Table_EmojiStatus + case let .wallpaper(value): + return strings.ChannelBoost_Table_Wallpaper(value) + case .customWallpaper: + return strings.ChannelBoost_Table_CustomWallpaper + } + } + + var iconName: String { + switch self { + case .story: + return "Premium/BoostPerk/Story" + case .reaction: + return "Premium/BoostPerk/Reaction" + case .nameColor: + return "Premium/BoostPerk/NameColor" + case .profileColor: + return "Premium/BoostPerk/CoverColor" + case .profileIcon: + return "Premium/BoostPerk/CoverLogo" + case .linkColor: + return "Premium/BoostPerk/LinkColor" + case .linkIcon: + return "Premium/BoostPerk/LinkLogo" + case .emojiStatus: + return "Premium/BoostPerk/EmojiStatus" + case .wallpaper: + return "Premium/BoostPerk/Wallpaper" + case .customWallpaper: + return "Premium/BoostPerk/CustomWallpaper" + } + } + } + + let theme: PresentationTheme + let strings: PresentationStrings + let level: Int32 + let isFirst: Bool + let perks: [Perk] + + init( + theme: PresentationTheme, + strings: PresentationStrings, + level: Int32, + isFirst: Bool, + perks: [Perk] + ) { + self.theme = theme + self.strings = strings + self.level = level + self.isFirst = isFirst + self.perks = perks + } + + static func ==(lhs: LevelSectionComponent, rhs: LevelSectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.level != rhs.level { + return false + } + if lhs.isFirst != rhs.isFirst { + return false + } + if lhs.perks != rhs.perks { + return false + } + return true + } + + static var body: Body { + let header = Child(LevelHeaderComponent.self) + let list = Child(List.self) + + return { context in + let component = context.component + + let header = header.update( + component: LevelHeaderComponent(theme: component.theme, text: component.isFirst ? component.strings.ChannelBoost_Table_LevelUnlocks(component.level) : component.strings.ChannelBoost_Table_Level(component.level)), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(header + .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height / 2.0))) + + let items: [AnyComponentWithIdentity] = component.perks.enumerated().map { index, value in + AnyComponentWithIdentity( + id: index, component: AnyComponent( + LevelPerkComponent( + theme: component.theme, + iconName: value.iconName, + text: value.title(strings: component.strings) + ) + ) + ) + } + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height + list.size.height / 2.0))) + + let height = header.size.height + list.size.height + return CGSize(width: context.availableSize.width, height: height) + } + } +} + + + +private final class LimitSheetContent: CombinedComponent { + typealias EnvironmentType = (Empty, ScrollChildEnvironment) + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + + let peer: EnginePeer + let subject: BoostSubject + let status: ChannelBoostStatus + + let copyLink: (String) -> Void + let dismiss: () -> Void + let openStats: (() -> Void)? + let openGift: (() -> Void)? + + init(context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + peer: EnginePeer, + subject: BoostSubject, + status: ChannelBoostStatus, + copyLink: @escaping (String) -> Void, + dismiss: @escaping () -> Void, + openStats: (() -> Void)?, + openGift: (() -> Void)? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.insets = insets + self.peer = peer + self.subject = subject + self.status = status + self.copyLink = copyLink + self.dismiss = dismiss + self.openStats = openStats + self.openGift = openGift + } + + static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.status != rhs.status { + return false + } + return true + } + + final class State: ComponentState { + var cachedChevronImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let text = Child(BalancedTextComponent.self) + let limit = Child(PremiumLimitDisplayComponent.self) + let linkButton = Child(SolidRoundedButtonComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + + let orLeftLine = Child(Rectangle.self) + let orRightLine = Child(Rectangle.self) + let orText = Child(MultilineTextComponent.self) + let giftText = Child(BalancedTextComponent.self) + + let levels = Child(List.self) + + return { context in + let component = context.component + let theme = component.theme + let strings = component.strings + + let state = context.state + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + let sideInset: CGFloat = 16.0 // + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 // + environment.safeInsets.left + + let iconName = "Premium/Boost" + let badgeText = "\(component.status.boosts)" + + var remaining: Int? + if let nextLevelBoosts = component.status.nextLevelBoosts { + remaining = nextLevelBoosts - component.status.boosts + } + + var textString = "" + let actionButtonText = strings.ChannelBoost_CopyLink + let buttonIconName = "Premium/CopyLink" + + if let remaining { + var needsSecondParagraph = true + let storiesString = strings.ChannelBoost_StoriesPerDay(Int32(component.status.level) + 1) + let valueString = strings.ChannelBoost_MoreBoosts(Int32(remaining)) + switch component.subject { + case .stories: + if component.status.level == 0 { + textString = strings.ChannelBoost_EnableStoriesText(valueString).string + } else { + textString = strings.ChannelBoost_IncreaseLimitText(valueString, storiesString).string + } + needsSecondParagraph = false + case let .channelReactions(reactionCount): + textString = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string + needsSecondParagraph = false + case .nameColors: + let colorLevel = component.subject.requiredLevel(context: context.component.context, configuration: premiumConfiguration) + + textString = strings.ChannelBoost_EnableNameColorLevelText("\(colorLevel)").string + case .nameIcon: + textString = strings.ChannelBoost_EnableNameIconLevelText("\(premiumConfiguration.minChannelNameIconLevel)").string + case .profileColors: + textString = strings.ChannelBoost_EnableProfileColorLevelText("\(premiumConfiguration.minChannelProfileColorLevel)").string + case .profileIcon: + textString = strings.ChannelBoost_EnableProfileIconLevelText("\(premiumConfiguration.minChannelProfileIconLevel)").string + case .emojiStatus: + textString = strings.ChannelBoost_EnableEmojiStatusLevelText("\(premiumConfiguration.minChannelEmojiStatusLevel)").string + case .wallpaper: + textString = strings.ChannelBoost_EnableWallpaperLevelText("\(premiumConfiguration.minChannelWallpaperLevel)").string + case .customWallpaper: + textString = strings.ChannelBoost_EnableCustomWallpaperLevelText("\(premiumConfiguration.minChannelCustomWallpaperLevel)").string + } + + if needsSecondParagraph { + textString += "\n\n\(strings.ChannelBoost_AskToBoost)" + } + } else { + let storiesString = strings.ChannelBoost_StoriesPerDay(Int32(component.status.level)) + textString = strings.ChannelBoost_MaxLevelReachedTextAuthor("\(component.status.level)", storiesString).string + } + + let defaultTitle = strings.ChannelBoost_Level("\(component.status.level)").string + let defaultValue = "" + let premiumValue = strings.ChannelBoost_Level("\(component.status.level + 1)").string + let premiumTitle = "" + + let progress: CGFloat + if let nextLevelBoosts = component.status.nextLevelBoosts { + progress = CGFloat(component.status.boosts - component.status.currentLevelBoosts) / CGFloat(nextLevelBoosts - component.status.currentLevelBoosts) + } else { + progress = 1.0 + } + + let contentSize: CGSize + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let textChild = text.update( + component: BalancedTextComponent( + text: .markdown(text: textString, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let gradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + let buttonGradientColors = [ + UIColor(rgb: 0x007afe), + UIColor(rgb: 0x5494ff) + ] + + let limitTransition: Transition = .immediate + + let button = button.update( + component: SolidRoundedButtonComponent( + title: actionButtonText, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: .black, + backgroundColors: buttonGradientColors, + foregroundColor: .white + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: buttonIconName, + animationName: nil, + iconPosition: .left, + action: { + component.copyLink(component.status.url) + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + var buttonOffset: CGFloat = 0.0 + var textOffset: CGFloat = 184.0 + + let linkButton = linkButton.update( + component: SolidRoundedButtonComponent( + title: component.status.url.replacingOccurrences(of: "https://", with: ""), + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), + backgroundColors: [], + foregroundColor: theme.list.itemPrimaryTextColor + ), + font: .regular, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + action: { + component.copyLink(component.status.url) + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + buttonOffset += 66.0 + + let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + textChild.size.height + 24.0), size: linkButton.size) + context.add(linkButton + .position(CGPoint(x: linkFrame.midX, y: linkFrame.midY)) + ) + + let textSize = textChild.size + textOffset += textSize.height / 2.0 + + context.add(textChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) + ) + + let limit = limit.update( + component: PremiumLimitDisplayComponent( + inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), + activeColors: gradientColors, + inactiveTitle: defaultTitle, + inactiveValue: defaultValue, + inactiveTitleColor: theme.list.itemPrimaryTextColor, + activeTitle: premiumTitle, + activeValue: premiumValue, + activeTitleColor: .white, + badgeIconName: iconName, + badgeText: badgeText, + badgePosition: progress, + badgeGraphPosition: progress, + invertProgress: true, + isPremiumDisabled: false + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: limitTransition + ) + context.add(limit + .position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0)) + ) + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(textSize.height / 2.0) + buttonOffset + 24.0), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + var additionalContentHeight: CGFloat = 0.0 + + if premiumConfiguration.giveawayGiftsPurchaseAvailable { + let orText = orText.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.ChannelBoost_Or, font: Font.regular(15.0), textColor: textColor.withAlphaComponent(0.8), paragraphAlignment: .center))), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(orText + .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 27.0)) + ) + + let orLeftLine = orLeftLine.update( + component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)), + availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel), + transition: .immediate + ) + context.add(orLeftLine + .position(CGPoint(x: context.availableSize.width / 2.0 - orText.size.width / 2.0 - 11.0 - 45.0, y: buttonFrame.maxY + 27.0)) + ) + + let orRightLine = orRightLine.update( + component: Rectangle(color: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)), + availableSize: CGSize(width: 90.0, height: 1.0 - UIScreenPixel), + transition: .immediate + ) + context.add(orRightLine + .position(CGPoint(x: context.availableSize.width / 2.0 + orText.size.width / 2.0 + 11.0 + 45.0, y: buttonFrame.maxY + 27.0)) + ) + + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + + let giftString = strings.Premium_BoostByGiftDescription2 + let giftAttributedString = parseMarkdownIntoAttributedString(giftString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + + if let range = giftAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + giftAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: giftAttributedString.string)) + } + let giftText = giftText.update( + component: BalancedTextComponent( + text: .plain(giftAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { _ in + return nil + }, + tapAction: { _, _ in + component.openGift?() + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(giftText + .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 50.0 + giftText.size.height / 2.0)) + ) + + additionalContentHeight += giftText.size.height + 50.0 + } + + + var nextLevels: ClosedRange? + if component.status.level < 10 { + nextLevels = Int32(component.status.level) + 1 ... 10 + } + + var levelsHeight: CGFloat = 0.0 + var levelItems: [AnyComponentWithIdentity] = [] + + var nameColorsAtLevel: [(Int32, Int32)] = [] + var nameColorsCountMap: [Int32: Int32] = [:] + for color in context.component.context.peerNameColors.displayOrder { + if let level = context.component.context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[color] { + if let current = nameColorsCountMap[level] { + nameColorsCountMap[level] = current + 1 + } else { + nameColorsCountMap[level] = 1 + } + } + } + for (key, value) in nameColorsCountMap { + nameColorsAtLevel.append((key, value)) + } + + if let nextLevels { + for level in nextLevels { + var perks: [LevelSectionComponent.Perk] = [] + perks.append(.story(level)) + perks.append(.reaction(level)) + + var nameColorsCount: Int32 = 0 + for (colorLevel, count) in nameColorsAtLevel { + if level >= colorLevel && colorLevel == 1 { + nameColorsCount = count + } + } + if nameColorsCount > 0 { + perks.append(.nameColor(nameColorsCount)) + } + + if level >= premiumConfiguration.minChannelProfileColorLevel { + let delta = min(level - premiumConfiguration.minChannelProfileColorLevel + 1, 2) + perks.append(.profileColor(8 * delta)) + } + if level >= premiumConfiguration.minChannelProfileIconLevel { + perks.append(.profileIcon) + } + + var linkColorsCount: Int32 = 0 + for (colorLevel, count) in nameColorsAtLevel { + if level >= colorLevel { + linkColorsCount += count + } + } + if linkColorsCount > 0 { + perks.append(.linkColor(linkColorsCount)) + } + + if level >= premiumConfiguration.minChannelNameIconLevel { + perks.append(.linkIcon) + } + if level >= premiumConfiguration.minChannelEmojiStatusLevel { + perks.append(.emojiStatus) + } + if level >= premiumConfiguration.minChannelWallpaperLevel { + perks.append(.wallpaper(8)) + } + if level >= premiumConfiguration.minChannelCustomWallpaperLevel { + perks.append(.customWallpaper) + } + + levelItems.append( + AnyComponentWithIdentity( + id: level, component: AnyComponent( + LevelSectionComponent( + theme: component.theme, + strings: component.strings, + level: level, + isFirst: levelItems.isEmpty, + perks: perks + ) + ) + ) + ) + } + } + + if !levelItems.isEmpty { + let levels = levels.update( + component: List(levelItems), + availableSize: CGSize(width: context.availableSize.width, height: 100000.0), + transition: context.transition + ) + context.add(levels + .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.maxY + 23.0 + additionalContentHeight + levels.size.height / 2.0 )) + ) + levelsHeight = levels.size.height + 40.0 + } + + let bottomInset: CGFloat = 0.0 + contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + additionalContentHeight + 5.0 + bottomInset + levelsHeight) + + return contentSize + } + } +} + + + +private final class BoostLevelsContainerComponent: CombinedComponent { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + + let peer: EnginePeer + let subject: BoostSubject + let status: ChannelBoostStatus + let copyLink: (String) -> Void + let dismiss: () -> Void + let openStats: () -> Void + let openGift: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer, + subject: BoostSubject, + status: ChannelBoostStatus, + copyLink: @escaping (String) -> Void, + dismiss: @escaping () -> Void, + openStats: @escaping () -> Void, + openGift: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.subject = subject + self.status = status + self.copyLink = copyLink + self.dismiss = dismiss + self.openStats = openStats + self.openGift = openGift + } + + static func ==(lhs: BoostLevelsContainerComponent, rhs: BoostLevelsContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.status != rhs.status { + return false + } + return true + } + + final class State: ComponentState { + var topContentOffset: CGFloat = 0.0 + var cachedStatsImage: (UIImage, PresentationTheme)? + var cachedCloseImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(Rectangle.self) + let scroll = Child(ScrollComponent.self) + let topPanel = Child(BlurredBackgroundComponent.self) + let topSeparator = Child(Rectangle.self) + let title = Child(MultilineTextComponent.self) + let statsButton = Child(Button.self) + let closeButton = Child(Button.self) + + return { context in + let state = context.state + + let theme = context.component.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let topInset: CGFloat = 56.0 + + let component = context.component + + let scroll = scroll.update( + component: ScrollComponent( + content: AnyComponent( + LimitSheetContent( + context: component.context, + theme: component.theme, + strings: component.strings, + insets: .zero, + peer: component.peer, + subject: component.subject, + status: component.status, + copyLink: component.copyLink, + dismiss: component.dismiss, + openStats: component.openStats, + openGift: component.openGift + ) + ), + contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 34.0, right: 0.0), + contentOffsetUpdated: { [weak state] topContentOffset, _ in + state?.topContentOffset = topContentOffset + Queue.mainQueue().justDispatch { + state?.updated(transition: .immediate) + } + }, + contentOffsetWillCommit: { _ in } + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let background = background.update( + component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor), + availableSize: scroll.size, + transition: context.transition + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + context.add(scroll + .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) + ) + + let topPanel = topPanel.update( + component: BlurredBackgroundComponent( + color: theme.rootController.navigationBar.blurredBackgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: topInset), + transition: context.transition + ) + + let topSeparator = topSeparator.update( + component: Rectangle( + color: theme.rootController.navigationBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let titleString: String + if let _ = component.status.nextLevelBoosts { + switch component.subject { + case .stories: + if component.status.level == 0 { + titleString = strings.ChannelBoost_EnableStories + } else { + titleString = strings.ChannelBoost_IncreaseLimit + } + case .nameColors: + titleString = strings.ChannelBoost_NameColor + case .nameIcon: + titleString = strings.ChannelBoost_NameIcon + case .profileColors: + titleString = strings.ChannelBoost_ProfileColor + case .profileIcon: + titleString = strings.ChannelBoost_ProfileIcon + case .channelReactions: + titleString = strings.ChannelBoost_CustomReactions + case .emojiStatus: + titleString = strings.ChannelBoost_EmojiStatus + case .wallpaper: + titleString = strings.ChannelBoost_Wallpaper + case .customWallpaper: + titleString = strings.ChannelBoost_CustomWallpaper + } + } else { + titleString = strings.ChannelBoost_MaxLevelReached + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0 + context.add(topPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + .opacity(topPanelAlpha) + ) + context.add(topSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) + .opacity(topPanelAlpha) + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + ) + + let statsButton = statsButton.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Premium/Stats", + tintColor: component.theme.list.itemAccentColor + ) + ), + action: { + component.dismiss() + Queue.mainQueue().after(0.35) { + component.openStats() + } + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(statsButton + .position(CGPoint(x: 31.0, y: 28.0)) + ) + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === component.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + ) + + return scroll.size + } + } +} + +public class PremiumBoostLevelsScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: PremiumBoostLevelsScreen? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + + let contentView: ComponentHostView + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: ContainerViewLayout? + + var isPremium: Bool? + var disposable: Disposable? + + init(context: AccountContext, controller: PremiumBoostLevelsScreen) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + if controller.forceDark { + self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) + } + self.presentationData = self.presentationData.withUpdated(theme: self.presentationData.theme.withModalBlocksBackground()) + + self.controller = controller + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.contentView = ComponentHostView() + + super.init() + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.contentView) + } + + deinit { + self.disposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let layout = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + if let scrollView = otherGestureRecognizer.view as? UIScrollView { + if scrollView.contentSize.width > scrollView.contentSize.height { + return false + } + } + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + + private var dismissOffset: CGFloat? + func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + self.currentLayout = layout + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else if let dismissOffset = self.dismissOffset, !dismissOffset.isZero { + topInset = edgeTopInset * dismissOffset + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + + self.updated(transition: transition) + } + + func updated(transition: Transition) { + guard let controller = self.controller else { + return + } + let containerSize = self.containerView.bounds.size + + + let contentSize = self.contentView.update( + transition: .immediate, + component: AnyComponent( + BoostLevelsContainerComponent( + context: controller.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + peer: controller.peer, + subject: controller.subject, + status: controller.status, + copyLink: { [weak self, weak controller] link in + guard let self else { + return + } + UIPasteboard.general.string = link + + if let previousController = controller?.navigationController?.viewControllers.reversed().first(where: { $0 !== controller }) as? ViewController { + previousController.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(text: self.presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + }, + dismiss: { [weak controller] in + controller?.dismiss(animated: true) + }, + openStats: controller.openStats, + openGift: controller.openGift + ) + ), + environment: {}, + containerSize: CGSize(width: containerSize.width, height: containerSize.height) + ) + self.contentView.frame = CGRect(origin: .zero, size: contentSize) + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let layout = self.currentLayout else { + return 210.0 + } + if case .compact = layout.metrics.widthClass { + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : bottomPanelPadding + let panelHeight: CGFloat = bottomPanelPadding + 50.0 + bottomInset + 28.0 + + let additionalInset: CGFloat = 0.0 + return layout.size.height - layout.size.width - 181.0 - panelHeight + additionalInset + } else { + return 210.0 + } + } + + private func findVerticalScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView, view.contentSize.height > view.contentSize.width { + return (view, nil) + } + return findVerticalScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let layout = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findVerticalScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } + let scrollView = scrollViewAndListNode?.0 + let listNode = scrollViewAndListNode?.1 + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) + case .changed: + guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if case let .known(value) = visibleContentOffset, value <= epsilon { + if let scrollView = scrollView { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + } + } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + if scrollView == nil { + translation = max(0.0, translation) + } + + self.panGestureArguments = (topInset, translation, scrollView, listNode) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if case let .known(value) = visibleContentOffset, value > 0.1 { + velocity = CGPoint() + } else if case .unknown = visibleContentOffset { + velocity = CGPoint() + } else if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { + DispatchQueue.main.async { + listNode.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 }) + } + } + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } else { + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func updateDismissOffset(_ offset: CGFloat) { + guard self.isExpanded, let layout = self.currentLayout else { + return + } + + self.dismissOffset = offset + self.containerLayoutUpdated(layout: layout, transition: .immediate) + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.dismissOffset = nil + self.isExpanded = isExpanded + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let peer: EnginePeer + private let subject: BoostSubject + private let status: ChannelBoostStatus + private let openStats: () -> Void + private let openGift: () -> Void + private let forceDark: Bool + + private var currentLayout: ContainerViewLayout? + + public var disposed: () -> Void = {} + + public init( + context: AccountContext, + peer: EnginePeer, + subject: BoostSubject, + status: ChannelBoostStatus, + openStats: @escaping () -> Void, + openGift: @escaping () -> Void, + forceDark: Bool = false + ) { + self.context = context + self.peer = peer + self.subject = subject + self.status = status + self.openStats = openStats + self.openGift = openGift + self.forceDark = forceDark + + super.init(navigationBarPresentationData: nil) + + self.navigationPresentation = .flatModal + self.statusBar.statusBarStyle = .Ignore + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + +// UIPasteboard.general.string = link +// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } +// self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + @objc private func cancelPressed() { + self.dismiss(animated: true, completion: nil) + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self) + self.displayNodeDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + self.node.containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index 2e2ef001cc7..6a27077fafe 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -234,8 +234,14 @@ public func PremiumBoostScreen( title: presentationData.strings.ChannelBoost_MoreBoosts_Title, text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { + dismissImpl?() + let controller = context.sharedContext.makePremiumGiftController(context: context) + pushController(controller) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], + actionLayout: .vertical, parseMarkdown: true ) presentController(controller) diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index 36236cfefe2..a6503bac26b 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -86,7 +86,9 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { var peerIds: [EnginePeer.Id] = [] switch subject { case let .giftCode(giftCode): - peerIds.append(giftCode.fromPeerId) + if let fromPeerId = giftCode.fromPeerId { + peerIds.append(fromPeerId) + } if let toPeerId = giftCode.toPeerId { peerIds.append(toPeerId) } @@ -201,7 +203,11 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { } link = "https://t.me/giftcode/\(giftCode.slug)" date = giftCode.date - fromPeer = state.peerMap[giftCode.fromPeerId] + if let fromPeerId = giftCode.fromPeerId { + fromPeer = state.peerMap[fromPeerId] + } else { + fromPeer = nil + } toPeerId = giftCode.toPeerId if let toPeerId = giftCode.toPeerId { toPeer = state.peerMap[toPeerId] @@ -285,7 +291,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { let linkButton = linkButton.update( component: Button( content: AnyComponent( - LinkButtonContentComponent(theme: environment.theme, text: link) + GiftLinkButtonContentComponent(theme: environment.theme, text: link) ), action: { if let link { @@ -744,25 +750,31 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer { } } -private final class LinkButtonContentComponent: CombinedComponent { +final class GiftLinkButtonContentComponent: CombinedComponent { let theme: PresentationTheme let text: String? + let isSeparateSection: Bool - public init( + init( theme: PresentationTheme, - text: String? + text: String?, + isSeparateSection: Bool = false ) { self.theme = theme self.text = text + self.isSeparateSection = isSeparateSection } - static func ==(lhs: LinkButtonContentComponent, rhs: LinkButtonContentComponent) -> Bool { + static func ==(lhs: GiftLinkButtonContentComponent, rhs: GiftLinkButtonContentComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.text != rhs.text { return false } + if lhs.isSeparateSection != rhs.isSeparateSection { + return false + } return true } @@ -778,7 +790,7 @@ private final class LinkButtonContentComponent: CombinedComponent { let sideInset: CGFloat = 38.0 let background = background.update( - component: RoundedRectangle(color: component.theme.list.itemInputField.backgroundColor, cornerRadius: 10.0), + component: RoundedRectangle(color: component.isSeparateSection ? component.theme.list.itemBlocksBackgroundColor : component.theme.list.itemInputField.backgroundColor, cornerRadius: 10.0), availableSize: context.availableSize, transition: context.transition ) @@ -1068,7 +1080,7 @@ private final class PeerCellComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) super.init(frame: frame) diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 88aacf28a88..b0f01da41fb 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -19,10 +19,12 @@ import InAppPurchaseManager import ConfettiEffect import TextFormat import UniversalMediaPlayer +import InstantPageCache public enum PremiumGiftSource: Equatable { case profile case attachMenu + case settings var identifier: String? { switch self { @@ -30,6 +32,8 @@ public enum PremiumGiftSource: Equatable { return "profile" case .attachMenu: return "attach" + case .settings: + return "settings" } } } @@ -39,20 +43,22 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let context: AccountContext let source: PremiumGiftSource - let peer: EnginePeer? + let peers: [EnginePeer] let products: [PremiumGiftProduct]? let selectedProductId: String? + let isCompleted: Bool let present: (ViewController) -> Void let selectProduct: (String) -> Void let buy: () -> Void - init(context: AccountContext, source: PremiumGiftSource, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) { + init(context: AccountContext, source: PremiumGiftSource, peers: [EnginePeer], products: [PremiumGiftProduct]?, selectedProductId: String?, isCompleted: Bool, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) { self.context = context self.source = source - self.peer = peer + self.peers = peers self.products = products self.selectedProductId = selectedProductId + self.isCompleted = isCompleted self.present = present self.selectProduct = selectProduct self.buy = buy @@ -65,7 +71,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if lhs.source != rhs.source { return false } - if lhs.peer != rhs.peer { + if lhs.peers != rhs.peers { return false } if lhs.products != rhs.products { @@ -74,7 +80,9 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if lhs.selectedProductId != rhs.selectedProductId { return false } - + if lhs.isCompleted != rhs.isCompleted { + return false + } return true } @@ -88,7 +96,10 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { private var stickersDisposable: Disposable? private var preloadDisposableSet = DisposableSet() + var cachedBoostIcon: UIImage? + var price: String? + var isCompleted = false init(context: AccountContext, source: PremiumGiftSource) { self.context = context @@ -167,8 +178,11 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { static var body: Body { let overscroll = Child(Rectangle.self) let text = Child(MultilineTextComponent.self) + let completedText = Child(MultilineTextComponent.self) let optionsSection = Child(SectionGroupComponent.self) + let perksTitle = Child(MultilineTextComponent.self) let perksSection = Child(SectionGroupComponent.self) + let termsText = Child(MultilineTextComponent.self) return { context in let sideInset: CGFloat = 16.0 @@ -186,7 +200,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { var size = CGSize(width: context.availableSize.width, height: 0.0) let overscroll = overscroll.update( - component: Rectangle(color: theme.list.plainBackgroundColor), + component: Rectangle(color: theme.list.blocksBackgroundColor), availableSize: CGSize(width: context.availableSize.width, height: 1000), transition: context.transition ) @@ -205,15 +219,77 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + var descriptionString: String = "" + if context.component.peers.count > 1 { + var names = "" + var more = "" + if context.component.peers.count < 4 { + for i in 0 ..< context.component.peers.count { + if i == 0 { + } else if i < context.component.peers.count - 1 { + names.append(strings.CreateGroup_PeersTitleDelimeter) + } else { + names.append(strings.CreateGroup_PeersTitleLastDelimeter) + } + names.append("**\(context.component.peers[i].compactDisplayTitle)**") + } + descriptionString = strings.Premium_Gift_MultipleDescription(names, "").string + } else { + for i in 0 ..< min(3, context.component.peers.count) { + if i == 0 { + + } else { + names.append(strings.CreateGroup_PeersTitleDelimeter) + } + names.append("**\(context.component.peers[i].compactDisplayTitle)**") + } + more = strings.Premium_Gift_NamesAndMore(Int32(context.component.peers.count - 3)) + } + if component.isCompleted { + descriptionString = strings.Premium_Gift_Sent_Multiple_Text(names, more).string + } else { + descriptionString = strings.Premium_Gift_MultipleDescription(names, more).string + } + } else { + if component.isCompleted { + descriptionString = strings.Premium_Gift_Sent_One_Text(component.peers.first?.compactDisplayTitle ?? "").string + } else { + descriptionString = strings.Premium_Gift_Description(component.peers.first?.compactDisplayTitle ?? "").string + } + } + + if !component.isCompleted { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + descriptionString += "\n\n" + descriptionString += environment.strings.Premium_Gift_YouWillReceiveBoosts(Int32(component.peers.count) * premiumConfiguration.boostsPerGiftCount).replacingOccurrences(of: "[]()", with: " [ ]() ") + } + + let boostIcon: UIImage + if let current = context.state.cachedBoostIcon { + boostIcon = current + } else { + boostIcon = generateImage(CGSize(width: 14.0, height: 20.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = UIImage(bundleImageName: "Premium/BoostChannel")?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + })! + context.state.cachedBoostIcon = boostIcon + } + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor, additionalAttributes: [NSAttributedString.Key.attachment.rawValue: boostIcon]), linkAttribute: { _ in return nil }) - let text = text.update( + let descriptionText = parseMarkdownIntoAttributedString(descriptionString, attributes: markdownAttributes, textAlignment: .center) + + let textComponent: _ConcreteChildComponent + if component.isCompleted { + textComponent = completedText + } else { + textComponent = text + } + let text = textComponent.update( component: MultilineTextComponent( - text: .markdown( - text: strings.Premium_Gift_Description(component.peer?.compactDisplayTitle ?? "").string, - attributes: markdownAttributes - ), + text: .plain(descriptionText), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -224,101 +300,111 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { ) context.add(text .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) ) size.height += text.size.height size.height += 21.0 var items: [SectionGroupComponent.Item] = [] - var i = 0 - if let products = component.products { - let gradientColors: [UIColor] = [ - UIColor(rgb: 0x8e77ff), - UIColor(rgb: 0x9a6fff), - UIColor(rgb: 0xb36eee) - ] - - let shortestOptionPrice: (Int64, NSDecimalNumber) - if let product = products.last { - shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months))) - } else { - shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) - } - - for product in products { - let giftTitle: String - if product.months == 12 { - giftTitle = strings.Premium_Gift_Years(1) - } else { - giftTitle = strings.Premium_Gift_Months(product.months) - } + + if !component.isCompleted { + if let products = component.products { + let gradientColors: [UIColor] = [ + UIColor(rgb: 0x8e77ff), + UIColor(rgb: 0x9a6fff), + UIColor(rgb: 0xb36eee) + ] - let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)) * 100.0) - let discount: String - if discountValue > 0 { - discount = "-\(discountValue)%" + let shortestOptionPrice: (Int64, NSDecimalNumber) + if let product = products.last { + shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months)), product.storeProduct.priceValue.dividing(by: NSDecimalNumber(value: product.months))) } else { - discount = "" + shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) } - let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(product.months)) - - var subtitle = "" - var accessibilitySubtitle = "" - var pricePerMonth = product.storeProduct.pricePerMonth(Int(product.months)) - pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string - - if discountValue > 0 { - subtitle = "**\(defaultPrice)** \(product.price)" - accessibilitySubtitle = product.price - } - - items.append(SectionGroupComponent.Item( - AnyComponentWithIdentity( - id: product.id, - component: AnyComponent( - PremiumOptionComponent( - title: giftTitle, - subtitle: subtitle, - labelPrice: pricePerMonth, - discount: discount, - selected: product.id == component.selectedProductId, - primaryTextColor: textColor, - secondaryTextColor: subtitleColor, - accentColor: gradientColors[i], - checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor, - checkBorderColor: environment.theme.list.itemCheckColors.strokeColor + for product in products { + let giftTitle: String + if product.months == 12 { + giftTitle = strings.Premium_Gift_Years(1) + } else { + giftTitle = strings.Premium_Gift_Months(product.months) + } + + let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(product.months) / Float(shortestOptionPrice.0)) * 100.0) + let discount: String + if discountValue > 0 { + discount = "-\(discountValue)%" + } else { + discount = "" + } + + let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(product.months)) + + var subtitle = "" + var accessibilitySubtitle = "" + var pricePerMonth = environment.strings.Premium_PricePerMonth(product.storeProduct.pricePerMonth(Int(product.months))).string + + if component.peers.count > 1 { + subtitle = "\(product.storeProduct.price) x \(component.peers.count)" + pricePerMonth = product.storeProduct.multipliedPrice(count: component.peers.count) + } else { + if discountValue > 0 { + subtitle = "**\(defaultPrice)** \(product.price)" + accessibilitySubtitle = product.price + } + } + + items.append(SectionGroupComponent.Item( + AnyComponentWithIdentity( + id: product.id, + component: AnyComponent( + PremiumOptionComponent( + title: giftTitle, + subtitle: subtitle, + labelPrice: pricePerMonth, + discount: discount, + multiple: component.peers.count > 1, + selected: product.id == component.selectedProductId, + primaryTextColor: textColor, + secondaryTextColor: subtitleColor, + accentColor: gradientColors[i], + checkForegroundColor: environment.theme.list.itemCheckColors.foregroundColor, + checkBorderColor: environment.theme.list.itemCheckColors.strokeColor + ) ) - ) - ), - accessibilityLabel: "\(giftTitle). \(accessibilitySubtitle). \(pricePerMonth)", - action: { - component.selectProduct(product.id) - }) - ) - i += 1 + ), + accessibilityLabel: "\(giftTitle). \(accessibilitySubtitle). \(pricePerMonth)", + action: { + component.selectProduct(product.id) + }) + ) + i += 1 + } } + + let optionsSection = optionsSection.update( + component: SectionGroupComponent( + items: items, + backgroundColor: environment.theme.list.itemBlocksBackgroundColor, + selectionColor: environment.theme.list.itemHighlightedBackgroundColor, + separatorColor: environment.theme.list.itemBlocksSeparatorColor + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(optionsSection + .position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + .disappear(.default(alpha: true)) + ) + size.height += optionsSection.size.height + size.height += 23.0 } - let optionsSection = optionsSection.update( - component: SectionGroupComponent( - items: items, - backgroundColor: environment.theme.list.itemBlocksBackgroundColor, - selectionColor: environment.theme.list.itemHighlightedBackgroundColor, - separatorColor: environment.theme.list.itemBlocksSeparatorColor - ), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(optionsSection - .position(CGPoint(x: availableWidth / 2.0, y: size.height + optionsSection.size.height / 2.0)) - .clipsToBounds(true) - .cornerRadius(10.0) - ) - size.height += optionsSection.size.height - size.height += 23.0 - let state = context.state let accountContext = context.component.context let present = context.component.present @@ -326,6 +412,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let price = context.component.products?.first(where: { $0.id == context.component.selectedProductId })?.price state.price = price + state.isCompleted = context.component.isCompleted let gradientColors: [UIColor] = [ UIColor(rgb: 0xef6922), @@ -347,6 +434,27 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0x3dbd4a) ] + let textSideInset: CGFloat = 16.0 + size.height += 8.0 + let perksTitle = perksTitle.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString(string: strings.Premium_WhatsIncluded.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor) + ), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(perksTitle + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + perksTitle.size.width / 2.0, y: size.height + perksTitle.size.height / 2.0)) + ) + size.height += perksTitle.size.height + size.height += 3.0 + i = 0 var perksItems: [SectionGroupComponent.Item] = [] for perk in state.configuration.perks { @@ -410,7 +518,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { } let buttonText: String - if let price = state?.price { + if let state, let price = state.price, !state.isCompleted { buttonText = strings.Premium_Gift_GiftSubscription(price).string } else { buttonText = strings.Common_OK @@ -419,7 +527,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .gift(state?.price), order: state?.configuration.perks, buttonText: buttonText, isPremium: false) controller.action = { [weak state] in dismissImpl?() - if let _ = state?.price { + if let state, let _ = state.price, !state.isCompleted { buy() } } @@ -453,8 +561,79 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { .clipsToBounds(true) .cornerRadius(10.0) ) - size.height += perksSection.size.height + size.height += 6.0 + + + let termsFont = Font.regular(13.0) + let termsTextColor = environment.theme.list.freeTextColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let termsString: MultilineTextComponent.TextContent = .markdown( + text: strings.Premium_Gift_Terms, + attributes: termsMarkdownAttributes + ) + + let controller = environment.controller + let termsTapActionImpl: ([NSAttributedString.Key: Any]) -> Void = { attributes in + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, + let controller = controller() as? PremiumGiftScreen, let navigationController = controller.navigationController as? NavigationController { + if url.hasPrefix("https://apps.apple.com/account/subscriptions") { + controller.context.sharedContext.applicationBindings.openSubscriptions() + } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { + controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + } else { + let context = controller.context + let signal: Signal? + switch url { + case "terms": + signal = cachedTermsPage(context: context) + case "privacy": + signal = cachedPrivacyPage(context: context) + default: + signal = nil + } + if let signal = signal { + let _ = (signal + |> deliverOnMainQueue).start(next: { resolvedUrl in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in + controller?.push(c) + }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + }) + } + } + } + } + + let termsText = termsText.update( + component: MultilineTextComponent( + text: termsString, + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + termsTapActionImpl(attributes) + } + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(termsText + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0)) + ) + size.height += termsText.size.height size.height += 10.0 size.height += scrollEnvironment.insets.bottom @@ -489,7 +668,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let peerId: PeerId + let peerIds: [EnginePeer.Id] let options: [CachedPremiumGiftOption] let source: PremiumGiftSource let buttonStatePromise: Promise @@ -499,10 +678,11 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let present: (ViewController) -> Void let push: (ViewController) -> Void let completion: (Int32) -> Void + let dismiss: () -> Void init( context: AccountContext, - peerId: PeerId, + peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, buttonStatePromise: Promise, @@ -511,10 +691,11 @@ private final class PremiumGiftScreenComponent: CombinedComponent { updateTabBarAlpha: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, - completion: @escaping (Int32) -> Void) - { + completion: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { self.context = context - self.peerId = peerId + self.peerIds = peerIds self.options = options self.source = source self.buttonStatePromise = buttonStatePromise @@ -524,13 +705,14 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.present = present self.push = push self.completion = completion + self.dismiss = dismiss } static func ==(lhs: PremiumGiftScreenComponent, rhs: PremiumGiftScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } - if lhs.peerId != rhs.peerId { + if lhs.peerIds != rhs.peerIds { return false } if lhs.options != rhs.options { @@ -544,7 +726,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext - private let peerId: PeerId + private let peerIds: [EnginePeer.Id] private let options: [CachedPremiumGiftOption] private let source: PremiumGiftSource private let buttonStatePromise: Promise @@ -552,6 +734,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void private let completion: (Int32) -> Void + private let dismiss: () -> Void var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? @@ -564,7 +747,9 @@ private final class PremiumGiftScreenComponent: CombinedComponent { } } - var peer: EnginePeer? + var isCompleted = false + + var peers: [EnginePeer.Id: EnginePeer] = [:] var products: [PremiumGiftProduct]? var selectedProductId: String? @@ -574,17 +759,18 @@ private final class PremiumGiftScreenComponent: CombinedComponent { init( context: AccountContext, - peerId: PeerId, + peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, buttonStatePromise: Promise, buttonAction: ActionSlot, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, - completion: @escaping (Int32) -> Void) - { + completion: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { self.context = context - self.peerId = peerId + self.peerIds = peerIds self.options = options self.source = source self.buttonAction = buttonAction @@ -592,6 +778,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.updateInProgress = updateInProgress self.present = present self.completion = completion + self.dismiss = dismiss super.init() @@ -605,8 +792,10 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - ).start(next: { [weak self] products, peer in + context.engine.data.get( + EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + ).start(next: { [weak self] products, peers in if let strongSelf = self { var gifts: [PremiumGiftProduct] = [] for option in strongSelf.options { @@ -619,7 +808,15 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if strongSelf.selectedProductId == nil && strongSelf.source != .attachMenu { strongSelf.selectedProductId = strongSelf.products?.first?.id } - strongSelf.peer = peer + + var unwrappedPeers: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, maybePeer) in peers { + if let peer = maybePeer { + unwrappedPeers[peerId] = peer + } + } + + strongSelf.peers = unwrappedPeers strongSelf.updated(transition: .immediate) } }) @@ -663,6 +860,11 @@ private final class PremiumGiftScreenComponent: CombinedComponent { return } + if self.isCompleted { + self.dismiss() + return + } + guard let product = self.products?.first(where: { $0.id == self.selectedProductId }) else { return } @@ -674,23 +876,42 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) - - let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerId, currency: currency, amount: amount) + + let purpose: AppStoreTransactionPurpose + var quantity: Int32 = 1 + if case .settings = self.source { + purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) + quantity = Int32(self.peerIds.count) + } else if let peerId = self.peerIds.first { + purpose = .gift(peerId: peerId, currency: currency, amount: amount) + } else { + fatalError() + } let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } if available { - strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose) + strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self, case .purchased = status { - Queue.mainQueue().after(2.0) { - let _ = updatePremiumPromoConfigurationOnce(account: strongSelf.context.account).start() - strongSelf.inProgress = false - strongSelf.updateInProgress(false) + if let self, case .purchased = status { + if case .settings = self.source { + self.inProgress = false + self.updateInProgress(false) + + self.isCompleted = true - strongSelf.updated(transition: .easeInOut(duration: 0.25)) - strongSelf.completion(duration) + self.updated(transition: .easeInOut(duration: 0.25)) + self.completion(duration) + } else { + Queue.mainQueue().after(2.0) { + let _ = updatePremiumPromoConfigurationOnce(account: self.context.account).start() + self.inProgress = false + self.updateInProgress(false) + + self.updated(transition: .easeInOut(duration: 0.25)) + self.completion(duration) + } } } }, error: { [weak self] error in @@ -741,14 +962,15 @@ private final class PremiumGiftScreenComponent: CombinedComponent { func makeState() -> State { return State( context: self.context, - peerId: self.peerId, + peerIds: self.peerIds, options: self.options, source: self.source, buttonStatePromise: self.buttonStatePromise, buttonAction: self.buttonAction, updateInProgress: self.updateInProgress, present: self.present, - completion: self.completion + completion: self.completion, + dismiss: self.dismiss ) } @@ -759,6 +981,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) + let completedTitle = Child(MultilineTextComponent.self) let secondaryTitle = Child(MultilineTextComponent.self) let bottomPanel = Child(BlurredBackgroundComponent.self) let bottomSeparator = Child(Rectangle.self) @@ -791,9 +1014,26 @@ private final class PremiumGiftScreenComponent: CombinedComponent { transition: context.transition ) - let title = title.update( + let titleString: String + if state.isCompleted { + if context.component.peerIds.count > 1 { + titleString = environment.strings.Premium_Gift_Sent_Multiple_Title + } else { + titleString = environment.strings.Premium_Gift_Sent_One_Title + } + } else { + titleString = environment.strings.Premium_Gift_Title + } + + let titleComponent: _ConcreteChildComponent + if state.isCompleted { + titleComponent = completedTitle + } else { + titleComponent = title + } + let title = titleComponent.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Premium_Gift_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: titleString, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -823,14 +1063,22 @@ private final class PremiumGiftScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) + var peers: [EnginePeer] = [] + for peerId in context.component.peerIds { + if let peer = state.peers[peerId] { + peers.append(peer) + } + } + let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumGiftScreenContentComponent( context: context.component.context, source: context.component.source, - peer: state.peer, + peers: peers, products: state.products, selectedProductId: state.selectedProductId, + isCompleted: state.isCompleted, present: context.component.present, selectProduct: { [weak state] productId in state?.selectProduct(id: productId) @@ -887,7 +1135,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let star = star.update( component: GiftAvatarComponent( context: context.component.context, - peer: context.state.peer, + theme: environment.theme, + peers: peers, isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations ), @@ -913,6 +1162,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0))) .scale(titleScale) .opacity(titleAlpha) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) ) context.add(secondaryTitle @@ -923,7 +1174,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let price: String? if let products = state.products, let selectedProductId = state.selectedProductId, let product = products.first(where: { $0.id == selectedProductId }) { - price = product.price + price = product.storeProduct.multipliedPrice(count: context.component.peerIds.count) } else { price = nil } @@ -939,9 +1190,22 @@ private final class PremiumGiftScreenComponent: CombinedComponent { context.component.updateTabBarAlpha(bottomPanelAlpha, .immediate) } else { let sideInset: CGFloat = 16.0 + + var gloss = true + let buttonText: String + if state.isCompleted { + buttonText = environment.strings.Premium_Gift_Sent_Close + gloss = false + } else if context.component.peerIds.count > 1 { + let subscriptions = environment.strings.Premium_Gift_GiftMultipleSubscriptions(Int32(context.component.peerIds.count)) + buttonText = environment.strings.Premium_Gift_GiftMultipleSubscriptionsFormat(subscriptions, price ?? "—").string + } else { + buttonText = environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string + } + let button = button.update( component: SolidRoundedButtonComponent( - title: environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string, + title: buttonText, theme: SolidRoundedButtonComponent.Theme( backgroundColor: UIColor(rgb: 0x8878ff), backgroundColors: [ @@ -954,7 +1218,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { ), height: 50.0, cornerRadius: 11.0, - gloss: true, + gloss: gloss, isLoading: state.inProgress, action: { state.buy() @@ -1015,7 +1279,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { public let mainButtonStatePromise = Promise(nil) private let mainButtonActionSlot = ActionSlot() - public init(context: AccountContext, peerId: PeerId, options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + public init(context: AccountContext, peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { self.context = context var updateInProgressImpl: ((Bool) -> Void)? @@ -1023,9 +1287,11 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { var pushImpl: ((ViewController) -> Void)? var completionImpl: ((Int32) -> Void)? var updateTabBarAlphaImpl: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var dismissImpl: (() -> Void)? + super.init(context: context, component: PremiumGiftScreenComponent( context: context, - peerId: peerId, + peerIds: peerIds, options: options, source: source, buttonStatePromise: self.mainButtonStatePromise, @@ -1044,6 +1310,9 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { }, completion: { duration in completionImpl?(duration) + }, + dismiss: { + dismissImpl?() } ), navigationBarAppearance: .transparent, presentationMode: .modal) @@ -1069,13 +1338,22 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { pushController(c) } - completionImpl = { _ in + completionImpl = { [weak self] _ in completion() + + if let self, case .settings = source { + self.animateSuccess() + } } - updateTabBarAlphaImpl = { [weak self] alpha, transition in self?.updateTabBarAlpha(alpha, transition) } + + dismissImpl = { [weak self] in + if let self { + self.dismiss() + } + } } required public init(coder aDecoder: NSCoder) { @@ -1086,6 +1364,10 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { self.dismiss() } + public func animateSuccess() { + self.view.addSubview(ConfettiView(frame: self.view.bounds)) + } + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index fe83c075c6f..85afd4aadad 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -29,6 +29,8 @@ import CheckNode import AnimationCache import MultiAnimationRenderer import TelegramNotices +import UndoUI +import TelegramStringFormatting public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -147,8 +149,8 @@ public enum PremiumSource: Equatable { } else { return false } - case let .gift(from, to, duration): - if case .gift(from, to, duration) = rhs { + case let .gift(from, to, duration, slug): + if case .gift(from, to, duration, slug) = rhs { return true } else { return false @@ -277,7 +279,7 @@ public enum PremiumSource: Equatable { case deeplink(String?) case profile(EnginePeer.Id) case emojiStatus(EnginePeer.Id, Int64, TelegramMediaFile?, LoadedStickerPack?) - case gift(from: EnginePeer.Id, to: EnginePeer.Id, duration: Int32) + case gift(from: EnginePeer.Id, to: EnginePeer.Id, duration: Int32, giftCode: PremiumGiftCodeInfo?) case giftTerms case voiceToText case fasterDownload @@ -683,6 +685,7 @@ final class PremiumOptionComponent: CombinedComponent { let subtitle: String let labelPrice: String let discount: String + let multiple: Bool let selected: Bool let primaryTextColor: UIColor let secondaryTextColor: UIColor @@ -695,6 +698,7 @@ final class PremiumOptionComponent: CombinedComponent { subtitle: String, labelPrice: String, discount: String, + multiple: Bool = false, selected: Bool, primaryTextColor: UIColor, secondaryTextColor: UIColor, @@ -706,6 +710,7 @@ final class PremiumOptionComponent: CombinedComponent { self.subtitle = subtitle self.labelPrice = labelPrice self.discount = discount + self.multiple = multiple self.selected = selected self.primaryTextColor = primaryTextColor self.secondaryTextColor = secondaryTextColor @@ -727,6 +732,9 @@ final class PremiumOptionComponent: CombinedComponent { if lhs.discount != rhs.discount { return false } + if lhs.multiple != rhs.multiple { + return false + } if lhs.selected != rhs.selected { return false } @@ -790,42 +798,8 @@ final class PremiumOptionComponent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right - label.size.width, height: context.availableSize.height), transition: context.transition ) - - var spacing: CGFloat = 0.0 - var subtitleSize = CGSize() - if !component.subtitle.isEmpty { - spacing = 2.0 - - let subtitleFont = Font.regular(13) - let subtitleColor = component.secondaryTextColor - - let subtitleString = parseMarkdownIntoAttributedString( - component.subtitle, - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), - bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]), - link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), - linkAttribute: { _ in return nil } - ) - ) - - let subtitle = subtitle.update( - component: MultilineTextComponent( - text: .plain(subtitleString), - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - context.add(subtitle - .position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) - ) - subtitleSize = subtitle.size - - insets.top -= 2.0 - insets.bottom -= 2.0 - } - + + var discountOffset: CGFloat = 0.0 let discountSize: CGSize if !component.discount.isEmpty { let discount = discount.update( @@ -833,7 +807,7 @@ final class PremiumOptionComponent: CombinedComponent { text: .plain( NSAttributedString( string: component.discount, - font: Font.with(size: 14.0, design: .round, weight: .semibold, traits: []), + font: Font.with(size: component.multiple ? 13.0 : 14.0, design: .round, weight: .semibold, traits: []), textColor: .white ) ), @@ -854,17 +828,59 @@ final class PremiumOptionComponent: CombinedComponent { transition: context.transition ) + let discountPosition: CGPoint + if component.multiple { + discountOffset = discountSize.width + 6.0 + discountPosition = CGPoint(x: insets.left + discountSize.width / 2.0, y: insets.top + title.size.height + discountSize.height / 2.0) + } else { + discountPosition = CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0) + } + context.add(discountBackground - .position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0)) + .position(discountPosition) ) - context.add(discount - .position(CGPoint(x: insets.left + title.size.width + 6.0 + discountSize.width / 2.0, y: insets.top + title.size.height / 2.0)) + .position(discountPosition) ) } else { discountSize = CGSize(width: 0.0, height: 18.0) } + var spacing: CGFloat = 0.0 + var subtitleSize = CGSize() + if !component.subtitle.isEmpty { + spacing = 2.0 + + let subtitleFont = Font.regular(13) + let subtitleColor = component.secondaryTextColor + + let subtitleString = parseMarkdownIntoAttributedString( + component.subtitle, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), + bold: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor, additionalAttributes: [NSAttributedString.Key.strikethroughStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber]), + link: MarkdownAttributeSet(font: subtitleFont, textColor: subtitleColor), + linkAttribute: { _ in return nil } + ) + ) + + let subtitle = subtitle.update( + component: MultilineTextComponent( + text: .plain(subtitleString), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + context.add(subtitle + .position(CGPoint(x: insets.left + subtitle.size.width / 2.0 + discountOffset, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) + ) + subtitleSize = subtitle.size + + insets.top -= 2.0 + insets.bottom -= 2.0 + } + let check = check.update( component: CheckComponent( theme: CheckComponent.Theme( @@ -1415,6 +1431,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let selectProduct: (String) -> Void let buy: () -> Void let updateIsFocused: (Bool) -> Void + let copyLink: (String) -> Void + let shareLink: (String) -> Void init( context: AccountContext, @@ -1430,7 +1448,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, - updateIsFocused: @escaping (Bool) -> Void + updateIsFocused: @escaping (Bool) -> Void, + copyLink: @escaping (String) -> Void, + shareLink: @escaping (String) -> Void ) { self.context = context self.source = source @@ -1446,6 +1466,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { self.selectProduct = selectProduct self.buy = buy self.updateIsFocused = updateIsFocused + self.copyLink = copyLink + self.shareLink = shareLink } static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool { @@ -1614,7 +1636,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let overscroll = Child(Rectangle.self) let fade = Child(RoundedRectangle.self) let text = Child(MultilineTextComponent.self) + let completedText = Child(MultilineTextComponent.self) + let linkButton = Child(Button.self) let optionsSection = Child(SectionGroupComponent.self) + let perksTitle = Child(MultilineTextComponent.self) let perksSection = Child(SectionGroupComponent.self) let infoBackground = Child(RoundedRectangle.self) let infoTitle = Child(MultilineTextComponent.self) @@ -1681,18 +1706,27 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) - // MARK: Nicegram change (let to var) - var textString: String + var link = "" + let textString: String if case .emojiStatus = context.component.source { textString = strings.Premium_EmojiStatusText.replacingOccurrences(of: "#", with: "# ") } else if case .giftTerms = context.component.source { textString = strings.Premium_PersonalDescription } else if let _ = context.component.otherPeerName { - if case let .gift(fromId, _, _) = context.component.source { + if case let .gift(fromId, _, _, giftCode) = context.component.source { if fromId == context.component.context.account.peerId { textString = strings.Premium_GiftedDescriptionYou } else { - textString = strings.Premium_GiftedDescription + if let giftCode { + if let _ = giftCode.usedDate { + textString = strings.Premium_Gift_UsedLink_Text + } else { + link = "https://t.me/giftcode/\(giftCode.slug)" + textString = strings.Premium_Gift_Link_Text + } + } else { + textString = strings.Premium_GiftedDescription + } } } else { textString = strings.Premium_PersonalDescription @@ -1706,18 +1740,19 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } else { textString = strings.Premium_Description } - // MARK: Nicegram - let locale = strings.baseLanguageCode - if context.component.isPremium != true { - textString = l("TelegramPremium.Description", locale) - } - // - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { _ in - return nil + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) }) - let text = text.update( + let shareLink = context.component.shareLink + let textComponent: _ConcreteChildComponent + if context.component.justBought { + textComponent = completedText + } else { + textComponent = text + } + let text = textComponent.update( component: MultilineTextComponent( text: .markdown( text: textString, @@ -1725,7 +1760,18 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.2 + lineSpacing: 0.2, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + shareLink(link) + } ), environment: {}, availableSize: CGSize(width: availableWidth - sideInsets - 8.0, height: 240.0), @@ -1733,16 +1779,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ) context.add(text .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) ) size.height += text.size.height size.height += 21.0 - // MARK: Nicegram - if context.component.isPremium != true { - return size - } - // - let gradientColors: [UIColor] = [ UIColor(rgb: 0xef6922), UIColor(rgb: 0xe95a2c), @@ -1831,7 +1873,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { accessibilitySubtitle = subtitle } pricePerMonth = environment.strings.Premium_PricePerMonth(pricePerMonth).string - + optionsItems.append( SectionGroupComponent.Item( AnyComponentWithIdentity( @@ -1887,8 +1929,30 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } + let textSideInset: CGFloat = 16.0 + let forceDark = context.component.forceDark let layoutPerks = { + size.height += 8.0 + let perksTitle = perksTitle.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString(string: strings.Premium_WhatsIncluded.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor) + ), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(perksTitle + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + perksTitle.size.width / 2.0, y: size.height + perksTitle.size.height / 2.0)) + ) + size.height += perksTitle.size.height + size.height += 3.0 + var i = 0 var perksItems: [SectionGroupComponent.Item] = [] for perk in state.configuration.perks { @@ -2008,14 +2072,38 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } + let copyLink = context.component.copyLink if case .emojiStatus = context.component.source { layoutPerks() layoutOptions() + } else if case let .gift(fromPeerId, _, _, giftCode) = context.component.source { + if let giftCode, fromPeerId != context.component.context.account.peerId, !context.component.justBought { + let link = "https://t.me/giftcode/\(giftCode.slug)" + let linkButton = linkButton.update( + component: Button( + content: AnyComponent( + GiftLinkButtonContentComponent(theme: environment.theme, text: link, isSeparateSection: true) + ), + action: { + copyLink(link) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: .immediate + ) + context.add(linkButton + .position(CGPoint(x: availableWidth / 2.0, y: size.height + linkButton.size.height / 2.0)) + .disappear(.default(alpha: true)) + ) + size.height += linkButton.size.height + size.height += 17.0 + } + + layoutPerks() } else { layoutOptions() layoutPerks() - let textSideInset: CGFloat = 16.0 let textPadding: CGFloat = 13.0 let infoTitle = infoTitle.update( @@ -2081,7 +2169,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { }) var isGiftView = false - if case let .gift(fromId, _, _) = context.component.source { + if case let .gift(fromId, _, _, _) = context.component.source { if fromId == context.component.context.account.peerId { isGiftView = true } @@ -2182,13 +2270,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void let push: (ViewController) -> Void - // MARK: Nicegram - let dismiss: () -> Void - // let completion: () -> Void + let copyLink: (String) -> Void + let shareLink: (String) -> Void - // MARK: Nicegram (dismiss) - init(context: AccountContext, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, dismiss: @escaping () -> Void, completion: @escaping () -> Void) { + init(context: AccountContext, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) { self.context = context self.source = source self.forceDark = forceDark @@ -2196,10 +2282,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.updateInProgress = updateInProgress self.present = present self.push = push - // MARK: Nicegram - self.dismiss = dismiss - // self.completion = completion + self.copyLink = copyLink + self.shareLink = shareLink } static func ==(lhs: PremiumIntroScreenComponent, rhs: PremiumIntroScreenComponent) -> Bool { @@ -2220,11 +2305,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let source: PremiumSource private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void - // MARK: Nicegram - private let dismiss: () -> Void - // private let completion: () -> Void var topContentOffset: CGFloat? @@ -2277,14 +2360,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } - // MARK: Nicegram (dismiss) - init(context: AccountContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, dismiss: @escaping () -> Void, completion: @escaping () -> Void) { + init(context: AccountContext, source: PremiumSource, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { self.context = context + self.source = source self.updateInProgress = updateInProgress self.present = present - // MARK: Nicegram - self.dismiss = dismiss - // self.completion = completion self.animationCache = context.animationCache @@ -2302,7 +2382,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let otherPeerName: Signal - if case let .gift(fromPeerId, toPeerId, _) = source { + if case let .gift(fromPeerId, toPeerId, _, _) = source { let otherPeerId = fromPeerId != context.account.peerId ? fromPeerId : toPeerId otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: otherPeerId)) |> map { peer -> String? in @@ -2391,14 +2471,70 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } func buy() { + guard !self.inProgress else { + return + } + + if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil { + self.inProgress = true + self.updateInProgress(true) + self.updated(transition: .immediate) + + self.paymentDisposable.set((self.context.engine.payments.applyPremiumGiftCode(slug: giftCode.slug) + |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self else { + return + } + + self.inProgress = false + self.updateInProgress(false) + self.updated(transition: .immediate) + + if case let .waitForExpiration(date) = error { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let dateText = stringForMediumDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Title, text: presentationData.strings.Premium_Gift_ApplyLink_AlreadyHasPremium_Text(dateText).string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, action: { _ in return true })) + } + }, completed: { [weak self] in + guard let self else { + return + } + + self.inProgress = false + self.justBought = true + self.updateInProgress(false) + self.updated(transition: .easeInOut(duration: 0.25)) + self.completion() + })) + return + } + // MARK: Nicegram - if isNicegram() { - dismiss() + guard !isNicegram() else { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let alert = UIAlertController( + title: nil, + message: l("TelegramPremium.Description"), + preferredStyle: .alert + ) + + alert.addAction( + UIAlertAction( + title: presentationData.strings.Common_OK, + style: .default + ) + ) + + UIApplication.topViewController?.present(alert, animated: true) + + return } // guard let inAppPurchaseManager = self.context.inAppPurchaseManager, - let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else { + let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }) else { return } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -2529,8 +2665,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } func makeState() -> State { - // MARK: Nicegram (dismiss) - return State(context: self.context, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, dismiss: self.dismiss, completion: self.completion) + return State(context: self.context, source: self.source, forceHasPremium: self.forceHasPremium, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) } static var body: Body { @@ -2658,7 +2793,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } else if case .profile = context.component.source { secondaryTitleText = environment.strings.Premium_PersonalTitle(otherPeerName).string - } else if case let .gift(fromPeerId, _, duration) = context.component.source { + } else if case let .gift(fromPeerId, _, duration, _) = context.component.source { if fromPeerId == context.component.context.account.peerId { if duration == 12 { secondaryTitleText = environment.strings.Premium_GiftedTitleYou_12Month(otherPeerName).string @@ -2777,7 +2912,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { }, updateIsFocused: { [weak state] isFocused in state?.updateIsFocused(isFocused) - } + }, + copyLink: context.component.copyLink, + shareLink: context.component.shareLink )), contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in @@ -2861,18 +2998,33 @@ private final class PremiumIntroScreenComponent: CombinedComponent { .opacity(max(0.0, 1.0 - titleAlpha * 1.8)) ) - var isGiftView = false - if case let .gift(fromId, _, _) = context.component.source { - if fromId == context.component.context.account.peerId { - isGiftView = true + var isUnusedGift = false + if case let .gift(fromId, _, _, giftCode) = context.component.source { + if let giftCode, giftCode.usedDate == nil, fromId != context.component.context.account.peerId { + isUnusedGift = true } } - - if (state.isPremium == true && (!state.canUpgrade || state.justBought)) || isGiftView { - - } else { - // MARK: Nicegram, subscribe button text always = 'OK' - let buttonTitle = environment.strings.Common_OK + + var buttonIsHidden = true + if !state.justBought { + if isUnusedGift { + buttonIsHidden = false + } else if state.canUpgrade { + buttonIsHidden = false + } else if !(state.isPremium ?? false) { + buttonIsHidden = false + } + } + + if !buttonIsHidden { + let buttonTitle: String + if isUnusedGift { + buttonTitle = environment.strings.Premium_Gift_ApplyLink + } else if state.isPremium == true && state.canUpgrade { + buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string + } else { + buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string + } let sideInset: CGFloat = 16.0 let button = button.update( @@ -2986,10 +3138,9 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { var updateInProgressImpl: ((Bool) -> Void)? var pushImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)? - // MARK: Nicegram - var dismissImpl: (() -> Void)? - // var completionImpl: (() -> Void)? + var copyLinkImpl: ((String) -> Void)? + var shareLinkImpl: ((String) -> Void)? super.init(context: context, component: PremiumIntroScreenComponent( context: context, source: source, @@ -3004,13 +3155,14 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { push: { c in pushImpl?(c) }, - // MARK: Nicegram - dismiss: { - dismissImpl?() - }, - // completion: { completionImpl?() + }, + copyLink: { link in + copyLinkImpl?(link) + }, + shareLink: { link in + shareLinkImpl?(link) } ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) @@ -3033,30 +3185,86 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } presentImpl = { [weak self] c in - self?.present(c, in: .window(.root)) + if c is UndoOverlayController { + self?.present(c, in: .current) + } else { + self?.present(c, in: .window(.root)) + } } pushImpl = { [weak self] c in self?.push(c) } - // MARK: Nicegram - dismissImpl = { [weak self] in - self?.dismiss() - } - // - completionImpl = { [weak self] in if let self { self.animateSuccess() } } + + copyLinkImpl = { [weak self] link in + UIPasteboard.general.string = link + + guard let self else { + return + } + self.dismissAllTooltips() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current) + } + + shareLinkImpl = { [weak self] link in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + + let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + + let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true)) + peerSelectionController.peerSelected = { [weak peerSelectionController, weak navigationController] peer, threadId in + if let _ = peerSelectionController { + Queue.mainQueue().after(0.88) { + HapticFeedback().success() + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + + let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages) + |> deliverOnMainQueue).startStandalone() + if let peerSelectionController = peerSelectionController { + peerSelectionController.dismiss() + } + } + } + navigationController.pushViewController(peerSelectionController) + } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + } + @objc private func cancelPressed() { self.dismiss() self.wasDismissed?() diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 29573caeb3d..29c1e1dfbc1 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1180,6 +1180,7 @@ private final class LimitSheetContent: CombinedComponent { if let remaining { let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) let valueString = strings.ChannelBoost_MoreBoosts(remaining) + switch boostSubject { case .stories: if level == 0 { @@ -1189,9 +1190,12 @@ private final class LimitSheetContent: CombinedComponent { titleText = strings.ChannelBoost_IncreaseLimit string = strings.ChannelBoost_IncreaseLimitText(valueString, storiesString).string } - case .nameColors: + case let .nameColors(colors): titleText = strings.ChannelBoost_EnableColors - string = strings.ChannelBoost_EnableColorsLevelText("\(premiumConfiguration.minChannelNameColorLevel)").string + + let colorLevel = requiredBoostSubjectLevel(subject: .nameColors(colors: colors), context: component.context, configuration: premiumConfiguration) + + string = strings.ChannelBoost_EnableColorsLevelText("\(colorLevel)").string case let .channelReactions(reactionCount): titleText = strings.ChannelBoost_CustomReactions string = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string @@ -1778,7 +1782,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { public enum BoostSubject: Equatable { case stories - case nameColors + case nameColors(colors: PeerNameColor) case channelReactions(reactionCount: Int) } diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index b87262f2c02..048e0fe72a9 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -28,14 +28,16 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { let initiallySelectedSlot: Int32? let selectedSlotsUpdated: ([Int32]) -> Void let presentController: (ViewController) -> Void + let giftPremium: () -> Void - init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, initiallySelectedSlot: Int32?, selectedSlotsUpdated: @escaping ([Int32]) -> Void, presentController: @escaping (ViewController) -> Void) { + init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, initiallySelectedSlot: Int32?, selectedSlotsUpdated: @escaping ([Int32]) -> Void, presentController: @escaping (ViewController) -> Void, giftPremium: @escaping () -> Void) { self.context = context self.peerId = peerId self.myBoostStatus = myBoostStatus self.initiallySelectedSlot = initiallySelectedSlot self.selectedSlotsUpdated = selectedSlotsUpdated self.presentController = presentController + self.giftPremium = giftPremium } static func ==(lhs: ReplaceBoostScreenComponent, rhs: ReplaceBoostScreenComponent) -> Bool { @@ -170,14 +172,26 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { if channelName.count > 48 { channelName = "\(channelName.prefix(48))..." } - let descriptionString = strings.ReassignBoost_Description(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string + let descriptionString = strings.ReassignBoost_DescriptionWithLink(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string + let giftPremium = context.component.giftPremium let description = description.update( component: MultilineTextComponent( text: .markdown(text: descriptionString, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.1 + lineSpacing: 0.1, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + giftPremium() + } ), environment: {}, availableSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset, height: availableSize.height), @@ -229,7 +243,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { selectionPosition: .right, isEnabled: isEnabled, hasNext: i != occupiedBoosts.count - 1, - action: { [weak state] _ in + action: { [weak state] _, _, _ in guard let state, hasSelection else { return } @@ -831,10 +845,13 @@ public class ReplaceBoostScreen: ViewController { var selectedSlotsUpdatedImpl: (([Int32]) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? + var giftPremiumImpl: (() -> Void)? self.init(context: context, component: ReplaceBoostScreenComponent(context: context, peerId: peerId, myBoostStatus: myBoostStatus, initiallySelectedSlot: initiallySelectedSlot, selectedSlotsUpdated: { slots in selectedSlotsUpdatedImpl?(slots) }, presentController: { c in presentControllerImpl?(c) + }, giftPremium: { + giftPremiumImpl?() })) self.title = presentationData.strings.ReassignBoost_Title @@ -856,6 +873,17 @@ public class ReplaceBoostScreen: ViewController { if let initiallySelectedSlot { self.node.selectedSlots = [initiallySelectedSlot] } + + giftPremiumImpl = { [weak self] in + guard let self else { + return + } + let navigationController = self.navigationController + self.dismiss(animated: true, completion: { + let giftController = context.sharedContext.makePremiumGiftController(context: context) + navigationController?.pushViewController(giftController, animated: true) + }) + } } private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { diff --git a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift index 2aa3acba777..18cb2f7ac77 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusNode.swift @@ -243,18 +243,21 @@ public final class RadialStatusNode: ASControlNode { } private let enableBlur: Bool - + private let isPreview: Bool + public private(set) var state: RadialStatusNodeState = .none + private var staticBackgroundNode: ASImageNode? private var backgroundNode: NavigationBackgroundNode? private var currentBackgroundNodeColor: UIColor? private var contentNode: RadialStatusContentNode? private var nextContentNode: RadialStatusContentNode? - public init(backgroundNodeColor: UIColor, enableBlur: Bool = false) { - self.enableBlur = enableBlur + public init(backgroundNodeColor: UIColor, enableBlur: Bool = false, isPreview: Bool = false) { self.backgroundNodeColor = backgroundNodeColor + self.enableBlur = enableBlur + self.isPreview = isPreview super.init() } @@ -325,6 +328,7 @@ public final class RadialStatusNode: ASControlNode { } else { self.contentNode = node if let contentNode = self.contentNode { + contentNode.displaysAsynchronously = self.displaysAsynchronously contentNode.frame = self.bounds contentNode.prepareAnimateIn(from: nil) self.addSubnode(contentNode) @@ -354,27 +358,40 @@ public final class RadialStatusNode: ASControlNode { if updated { if let color = color { - if let backgroundNode = self.backgroundNode { - backgroundNode.updateColor(color: color, transition: .immediate) - self.currentBackgroundNodeColor = color - - completion() - } else { - let backgroundNode = NavigationBackgroundNode(color: color, enableBlur: self.enableBlur) - self.currentBackgroundNodeColor = color - - backgroundNode.frame = self.bounds - backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate) - self.backgroundNode = backgroundNode - self.insertSubnode(backgroundNode, at: 0) - - if animated { - backgroundNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, removeOnCompletion: false) - backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - completion() - }) + if self.isPreview { + let backgroundNode: ASImageNode + if let current = self.staticBackgroundNode { + backgroundNode = current } else { + backgroundNode = ASImageNode() + backgroundNode.image = generateFilledCircleImage(diameter: 50.0, color: self.backgroundNodeColor) + self.insertSubnode(backgroundNode, at: 0) + self.staticBackgroundNode = backgroundNode + } + backgroundNode.frame = self.bounds + } else { + if let backgroundNode = self.backgroundNode { + backgroundNode.updateColor(color: color, transition: .immediate) + self.currentBackgroundNodeColor = color + completion() + } else { + let backgroundNode = NavigationBackgroundNode(color: color, enableBlur: self.enableBlur) + self.currentBackgroundNodeColor = color + + backgroundNode.frame = self.bounds + backgroundNode.update(size: backgroundNode.bounds.size, cornerRadius: backgroundNode.bounds.size.height / 2.0, transition: .immediate) + self.backgroundNode = backgroundNode + self.insertSubnode(backgroundNode, at: 0) + + if animated { + backgroundNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2, removeOnCompletion: false) + backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } else { + completion() + } } } } else if let backgroundNode = self.backgroundNode { diff --git a/submodules/Reachability/Sources/Reachability.swift b/submodules/Reachability/Sources/Reachability.swift index 2f0f53ab3bf..71e291824ea 100644 --- a/submodules/Reachability/Sources/Reachability.swift +++ b/submodules/Reachability/Sources/Reachability.swift @@ -84,7 +84,7 @@ private final class WrappedLegacyReachability: NSObject { } } -@available(iOSApplicationExtension 12.0, iOS 12.0, OSX 10.14, *) +@available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) private final class PathMonitor { private let queue: Queue private let monitor: NWPathMonitor @@ -133,7 +133,7 @@ private final class PathMonitor { } } -@available(iOSApplicationExtension 12.0, iOS 12.0, OSX 10.14, *) +@available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) private final class SharedPathMonitor { static let queue = Queue() static let impl = QueueLocalObject(queue: queue, generate: { @@ -149,7 +149,7 @@ public enum Reachability { } public static var networkType: Signal { - if #available(iOSApplicationExtension 12.0, iOS 12.0, OSX 10.14, *) { + if #available(iOSApplicationExtension 12.0, iOS 12.0, macOS 14.0, *) { return Signal { subscriber in let disposable = MetaDisposable() diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index a61a4d9ec11..0a2304340b3 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -589,6 +589,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strings: strongSelf.presentationData.strings, deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, + color: nil, backgroundColor: .clear, separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), hideTopPanel: hideTopPanel, @@ -1215,6 +1216,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { strings: self.presentationData.strings, deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, + color: nil, backgroundColor: .clear, separatorColor: self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), hideTopPanel: hideTopPanel, diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 67afdc211a1..e2a6db088d4 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -202,6 +202,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 4d01e62b3d3..2c277559c43 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -170,11 +170,11 @@ public final class SelectablePeerNode: ASDisplayNode { ) } - public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) { + public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, isMessage: Bool) { self.peer = nil self.textNode.maximumNumberOfLines = 2 - self.textNode.attributedText = NSAttributedString(string: strings.Share_RepostStory, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: isMessage ? strings.Share_RepostToStory : strings.Share_RepostStory, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center) self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: .repostIcon, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad) self.avatarNode.playRepostAnimation() diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 6bf6353f3d3..93d2f1e75d9 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -122,6 +122,13 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TelegramUI/Components/Settings/QuickReactionSetupController", + "//submodules/TelegramUI/Components/Settings/ThemeCarouselItem", + "//submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem", + "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", + "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", + "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", + "//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen", + "//submodules/TelegramUI/Components/Settings/GenerateThemeName", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index a8de19b0b0e..39af380686f 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -22,6 +22,7 @@ import QrCodeUI import PremiumUI import StorageUsageScreen import PeerInfoStoryGridScreen +import WallpaperGridScreen enum SettingsSearchableItemIcon { case profile diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 06dad8b58c7..39b8e378552 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,10 +222,11 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) @@ -307,10 +308,10 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let timestamp = self.referenceTimestamp diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index ea45e2a190a..cb70fa94eae 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -19,6 +19,8 @@ import ContextUI import UndoUI import ItemListPeerActionItem import AnimationUI +import ThemeSettingsThemeItem +import ThemeAccentColorScreen private final class ThemePickerControllerArguments { let context: AccountContext diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index b2d3d19c4de..ef7c648cb75 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -18,7 +18,7 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import StickerResources - +import ThemeCarouselItem private var cachedBorderImages: [String: UIImage] = [:] private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { @@ -193,10 +193,14 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { } let string: String? - if let _ = item.themeReference.emoticon { - string = nil + if let themeReference = item.themeReference { + if let _ = themeReference.emoticon { + string = nil + } else { + string = themeDisplayName(strings: item.strings, reference: themeReference) + } } else { - string = themeDisplayName(strings: item.strings, reference: item.themeReference) + string = nil } let text = NSAttributedString(string: string ?? item.strings.Conversation_Theme_NoTheme, font: Font.bold(14.0), textColor: .white) @@ -207,16 +211,17 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) if updatedThemeReference || updatedWallpaper || updatedNightMode || updatedSize { - var themeReference = item.themeReference - if case .builtin = themeReference, item.nightMode { - themeReference = .builtin(.night) + if var themeReference = item.themeReference { + if case .builtin = themeReference, item.nightMode { + themeReference = .builtin(.night) + } + + let color = item.themeSpecificAccentColors[themeReference.index] + let wallpaper = item.themeSpecificChatWallpapers[themeReference.index] + + self.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, emoticon: true, large: true)) + self.imageNode.backgroundColor = nil } - - let color = item.themeSpecificAccentColors[themeReference.index] - let wallpaper = item.themeSpecificChatWallpapers[themeReference.index] - - self.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, emoticon: true, large: true)) - self.imageNode.backgroundColor = nil } if updatedTheme || updatedSelected { @@ -500,8 +505,10 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { for theme in item.themes { let selected = item.currentTheme.index == theme.index - let iconItem = ThemeCarouselThemeIconItem(context: item.context, emojiFile: theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file }, themeReference: theme, nightMode: item.nightMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil, action: { theme in - item.updatedTheme(theme) + let iconItem = ThemeCarouselThemeIconItem(context: item.context, emojiFile: theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file }, themeReference: theme, nightMode: item.nightMode, channelMode: false, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil, action: { theme in + if let theme { + item.updatedTheme(theme) + } }, contextAction: nil) validIds.append(theme.index) diff --git a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift index 821ea25b204..d4ac7c9af53 100644 --- a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift +++ b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift @@ -10,8 +10,11 @@ import MediaResources import AccountContext import LegacyUI import LegacyMediaPickerUI +import LegacyComponents import LocalMediaResources import ImageBlur +import WallpaperGridScreen +import WallpaperGalleryScreen func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -46,349 +49,3 @@ func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (V present(legacyController) }) } - -func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, completion: @escaping () -> Void) { - var imageSignal: Signal - switch wallpaper { - case let .wallpaper(wallpaper, _): - switch wallpaper { - case let .file(file): - if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() - } - case let .image(representations, _): - for representation in representations { - let resource = representation.resource - if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - } - } - default: - break - } - imageSignal = .complete() - completion() - case let .asset(asset): - imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false) - |> filter { value in - return !(value?.1 ?? true) - } - |> mapToSignal { result -> Signal in - if let result = result { - return .single(result.0) - } else { - return .complete() - } - } - case let .contextResult(result): - var imageResource: TelegramMediaResource? - switch result { - case let .externalReference(externalReference): - if let content = externalReference.content { - imageResource = content.resource - } - case let .internalReference(internalReference): - if let image = internalReference.image { - if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { - imageResource = imageRepresentation.resource - } - } - } - - if let imageResource = imageResource { - imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) - |> mapToSignal { path -> Signal in - if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { - return .single(image) - } else { - return .complete() - } - } - } else { - imageSignal = .complete() - } - } - - if let editedImage { - imageSignal = .single(editedImage) - } - - let _ = (imageSignal - |> map { image -> UIImage in - var croppedImage = UIImage() - - let finalCropRect: CGRect - if let cropRect = cropRect { - finalCropRect = cropRect - } else { - let screenSize = TGScreenSize() - let fittedSize = TGScaleToFit(screenSize, image.size) - finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height) - } - croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true) - - let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0)) - let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) - - if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { - let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - - let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered - let accountManager = context.sharedContext.accountManager - let account = context.account - let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in - var resource: MediaResource? - if case let .image(representations, _) = wallpaper, let representation = largestImageRepresentation(representations) { - resource = representation.resource - } else if case let .file(file) = wallpaper { - resource = file.file.resource - } - - if let resource = resource { - let _ = accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) - let _ = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) - } - - let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - 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) - })).start() - } - - let apply: () -> Void = { - let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil) - let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) - updateWallpaper(wallpaper) - DispatchQueue.main.async { - completion() - } - } - - if mode.contains(.blur) { - let representation = CachedBlurredWallpaperRepresentation() - let _ = context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start() - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start(completed: { - apply() - }) - } else { - apply() - } - } - return croppedImage - }).start() -} - -public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, peerId: PeerId, forBoth: Bool, completion: @escaping () -> Void) { - var imageSignal: Signal - switch wallpaper { - case let .wallpaper(wallpaper, _): - imageSignal = .complete() - switch wallpaper { - case let .file(file): - if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) { - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true) - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() - - imageSignal = .single(image) - } - case let .image(representations, _): - for representation in representations { - let resource = representation.resource - if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() - } - } - default: - break - } - completion() - case let .asset(asset): - imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false) - |> filter { value in - return !(value?.1 ?? true) - } - |> mapToSignal { result -> Signal in - if let result = result { - return .single(result.0) - } else { - return .complete() - } - } - case let .contextResult(result): - var imageResource: TelegramMediaResource? - switch result { - case let .externalReference(externalReference): - if let content = externalReference.content { - imageResource = content.resource - } - case let .internalReference(internalReference): - if let image = internalReference.image { - if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { - imageResource = imageRepresentation.resource - } - } - } - - if let imageResource = imageResource { - imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) - |> mapToSignal { path -> Signal in - if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { - return .single(image) - } else { - return .complete() - } - } - } else { - imageSignal = .complete() - } - } - - if let editedImage { - imageSignal = .single(editedImage) - } - - let _ = (imageSignal - |> map { image -> UIImage in - var croppedImage = UIImage() - - let finalCropRect: CGRect - if let cropRect = cropRect { - finalCropRect = cropRect - } else { - let screenSize = TGScreenSize() - let fittedSize = TGScaleToFit(screenSize, image.size) - finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height) - } - croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true) - - let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0)) - let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) - - if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { - let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - - let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() - - var intensity: Int32? - if let brightness { - intensity = max(0, min(100, Int32(brightness * 100.0))) - } - - Queue.mainQueue().after(0.05) { - let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity) - let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) - - context.account.pendingPeerMediaUploadManager.add(peerId: peerId, content: .wallpaper(wallpaper: temporaryWallpaper, forBoth: forBoth)) - - Queue.mainQueue().after(0.05) { - completion() - } - } - } - return croppedImage - }).start() -} - -class LegacyWallpaperItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem { - var isVideo: Bool { - return false - } - - var uniqueIdentifier: String! { - return self.asset.localIdentifier - } - - let asset: PHAsset - let screenImage: UIImage - private(set) var thumbnailResource: TelegramMediaResource? - private(set) var imageResource: TelegramMediaResource? - let dimensions: CGSize - - - init(asset: PHAsset, screenImage: UIImage, dimensions: CGSize) { - self.asset = asset - self.screenImage = screenImage - self.dimensions = dimensions - } - - var originalSize: CGSize { - return self.dimensions - } - - func thumbnailImageSignal() -> SSignal! { - return SSignal.complete() -// 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 screenImageSignal(_ position: TimeInterval) -> SSignal! { - return SSignal.single(self.screenImage) - } - - var originalImage: Signal { - return fetchPhotoLibraryImage(localIdentifier: self.asset.localIdentifier, thumbnail: false) - |> filter { value in - return !(value?.1 ?? true) - } - |> mapToSignal { result -> Signal in - if let result = result { - return .single(result.0) - } else { - return .complete() - } - } - } - - 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() - }) - }) - } -} diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index c840a41700d..c8030edd2e2 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -14,6 +14,8 @@ import LegacyMediaPickerUI import WallpaperResources import AccountContext import MediaResources +import ThemeAccentColorScreen +import GenerateThemeName private final class EditThemeControllerArguments { let context: AccountContext diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index e1b9d76ff00..2f8ca0cdabd 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -14,6 +14,7 @@ import DeviceLocationManager import Geocoding import WallpaperResources import Sunrise +import ThemeSettingsThemeItem private enum TriggerMode { case system diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift deleted file mode 100644 index 12117ce26cd..00000000000 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerItem.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramCore -import SwiftSignalKit -import AsyncDisplayKit -import Postbox -import AccountContext -import GridMessageSelectionNode - -final class ThemeGridControllerItem: GridItem { - let context: AccountContext - let wallpaper: TelegramWallpaper - let wallpaperId: ThemeGridControllerEntry.StableId - let index: Int - let editable: Bool - let selected: Bool - let interaction: ThemeGridControllerInteraction - - let section: GridSection? = nil - - init(context: AccountContext, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) { - self.context = context - self.wallpaper = wallpaper - self.wallpaperId = wallpaperId - self.index = index - self.editable = editable - self.selected = selected - self.interaction = interaction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = ThemeGridControllerItemNode() - node.setup(item: self, synchronousLoad: synchronousLoad) - return node - } - - func update(node: GridItemNode) { - guard let node = node as? ThemeGridControllerItemNode else { - assertionFailure() - return - } - node.setup(item: self, synchronousLoad: false) - } -} - -final class ThemeGridControllerItemNode: GridItemNode { - private let wallpaperNode: SettingsThemeWallpaperNode - private var selectionNode: GridMessageSelectionNode? - - private var item: ThemeGridControllerItem? - - override init() { - self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) - - super.init() - - self.addSubnode(self.wallpaperNode) - } - - override func didLoad() { - super.didLoad() - - self.view.isExclusiveTouch = true - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) { - self.item = item - self.updateSelectionState(animated: false) - self.setNeedsLayout() - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let item = self.item { - item.interaction.openWallpaper(item.wallpaper) - } - } - } - - func updateSelectionState(animated: Bool) { - if let item = self.item { - let (editing, selectedIds) = item.interaction.selectionState - - if editing && item.editable { - let selected = selectedIds.contains(item.wallpaperId) - - if let selectionNode = self.selectionNode { - selectionNode.updateSelected(selected, animated: animated) - selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) - } else { - let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme - let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in - if let strongSelf = self { - strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value) - } - }) - - selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) - self.addSubnode(selectionNode) - self.selectionNode = selectionNode - selectionNode.updateSelected(selected, animated: false) - if animated { - selectionNode.animateIn() - } - } - } - else { - if let selectionNode = self.selectionNode { - self.selectionNode = nil - if animated { - selectionNode.animateOut { [weak selectionNode] in - selectionNode?.removeFromSupernode() - } - } else { - selectionNode.removeFromSupernode() - } - } - } - } - } - - override func layout() { - super.layout() - - let bounds = self.bounds - if let item = self.item { - self.wallpaperNode.setWallpaper(context: item.context, wallpaper: item.wallpaper, selected: item.selected, size: bounds.size, synchronousLoad: false) - self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) - } - } -} diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 59ebed6a4b1..9dd1e3c9647 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -14,6 +14,7 @@ import LegacyComponents import WallpaperBackgroundNode import AnimationCache import MultiAnimationRenderer +import WallpaperGalleryScreen private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -370,10 +371,11 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) func makeChatListItem( @@ -455,10 +457,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index a7740847bd3..22cd47d5712 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -19,32 +19,9 @@ import ContextUI import UndoUI import PremiumUI import PeerNameColorScreen - -func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { - let name: String - switch reference { - case let .builtin(theme): - switch theme { - case .dayClassic: - name = strings.Appearance_ThemeCarouselClassic - case .day: - name = strings.Appearance_ThemeCarouselDay - case .night: - name = strings.Appearance_ThemeCarouselNewNight - case .nightAccent: - name = strings.Appearance_ThemeCarouselTintedNight - } - case let .local(theme): - name = theme.title - case let .cloud(theme): - if let emoticon = theme.theme.emoticon { - name = emoticon - } else { - name = theme.theme.title - } - } - return name -} +import ThemeCarouselItem +import ThemeAccentColorScreen +import WallpaperGridScreen private final class ThemeSettingsControllerArguments { let context: AccountContext @@ -308,8 +285,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case let .chatPreview(theme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items): return ThemeSettingsChatPreviewItem(context: arguments.context, theme: theme, componentTheme: theme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items) case let .themes(theme, strings, chatThemes, currentTheme, nightMode, animatedEmojiStickers, themeSpecificAccentColors, themeSpecificChatWallpapers): - return ThemeCarouselThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, nightMode: nightMode, currentTheme: currentTheme, updatedTheme: { theme in - arguments.selectTheme(theme) + return ThemeCarouselThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: chatThemes, hasNoTheme: false, animatedEmojiStickers: animatedEmojiStickers, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, nightMode: nightMode, currentTheme: currentTheme, updatedTheme: { theme in + if let theme { + arguments.selectTheme(theme) + } }, contextAction: { theme, node, gesture in arguments.themeContextAction(false, theme, node, gesture) }, tag: ThemeSettingsEntryTag.theme) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 647108975ea..c21786a1e87 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -1074,7 +1074,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self?.switchToAnotherAccount?() }, debugAction: { [weak self] in self?.debugAction?() - }, extendedInitialReveal: self.presetText != nil, segmentedValues: self.segmentedValues) + }, extendedInitialReveal: self.presetText != nil, segmentedValues: self.segmentedValues, fromPublicChannel: self.fromPublicChannel) self.peersContentNode = peersContentNode peersContentNode.openSearch = { [weak self] in let _ = (_internal_recentlySearchedPeers(postbox: context.stateManager.postbox) diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index c48874744d8..ce8927fa6ff 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -95,7 +95,7 @@ final class ShareControllerGridSectionNode: ASDisplayNode { final class ShareControllerPeerGridItem: GridItem { enum ShareItem: Equatable { case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?) - case story + case story(isMessage: Bool) var peerId: EnginePeer.Id? { if case let .peer(peer, _, _, _) = self { @@ -246,14 +246,15 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.placeholderNode = nil shimmerNode.removeFromSupernode() } - } else if let item, case .story = item { + } else if let item, case let .story(isMessage) = item { self.peerNode.setupStoryRepost( accountPeerId: context.accountPeerId, postbox: context.stateManager.postbox, network: context.stateManager.network, theme: theme, strings: strings, - synchronousLoad: synchronousLoad + synchronousLoad: synchronousLoad, + isMessage: isMessage ) } else { let shimmerNode: ShimmerEffectNode diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 10c4b1d9316..15eaac5cd57 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -145,7 +145,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } private let tick = ValuePromise(0) - init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(EngineRenderedPeer, EnginePeer.Presence?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?) { + init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, switchableAccounts: [ShareControllerSwitchableAccount], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(EngineRenderedPeer, EnginePeer.Presence?)], accountPeer: EnginePeer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, debugAction: @escaping () -> Void, extendedInitialReveal: Bool, segmentedValues: [ShareControllerSegmentedValue]?, fromPublicChannel: Bool) { self.environment = environment self.context = context self.theme = theme @@ -170,7 +170,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { var index: Int32 = 0 if canShareStory { - entries.append(SharePeerEntry(index: index, item: .story, theme: theme, strings: strings)) + entries.append(SharePeerEntry(index: index, item: .story(isMessage: fromPublicChannel), theme: theme, strings: strings)) index += 1 } diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 31a05c84d98..dc9083423c9 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -1130,6 +1130,8 @@ public final class SolidRoundedButtonView: UIView { strongSelf.iconNode.alpha = 0.55 strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity") strongSelf.animationNode?.alpha = 0.55 + strongSelf.badgeNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.badgeNode?.alpha = 0.55 } else { if strongSelf.buttonBackgroundNode.alpha > 0.0 { strongSelf.buttonBackgroundNode.alpha = 1.0 @@ -1142,6 +1144,8 @@ public final class SolidRoundedButtonView: UIView { strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) strongSelf.animationNode?.alpha = 1.0 strongSelf.animationNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) + strongSelf.badgeNode?.alpha = 1.0 + strongSelf.badgeNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) } } } @@ -1461,11 +1465,33 @@ public final class SolidRoundedButtonView: UIView { let spacingOffset: CGFloat = 9.0 let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset let iconSpacing: CGFloat = self.iconSpacing + let badgeSpacing: CGFloat = 6.0 var contentWidth: CGFloat = titleSize.width if !iconSize.width.isZero { contentWidth += iconSize.width + iconSpacing } + + var badgeSize: CGSize = .zero + if let badge = self.badge { + let badgeNode: BadgeNode + if let current = self.badgeNode { + badgeNode = current + } else { + badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) + badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0 + self.badgeNode = badgeNode + self.addSubnode(badgeNode) + } + badgeNode.text = badge + badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0)) + + contentWidth += badgeSize.width + badgeSpacing + } else if let badgeNode = self.badgeNode { + self.badgeNode = nil + badgeNode.removeFromSupernode() + } + var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0) let iconFrame: CGRect @@ -1484,6 +1510,7 @@ public final class SolidRoundedButtonView: UIView { } iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) } + let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize) transition.updateFrame(view: self.iconNode, frame: iconFrame) if let animationNode = self.animationNode { @@ -1491,22 +1518,8 @@ public final class SolidRoundedButtonView: UIView { } transition.updateFrame(view: self.titleNode, frame: titleFrame) - if let badge = self.badge { - let badgeNode: BadgeNode - if let current = self.badgeNode { - badgeNode = current - } else { - badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) - badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0 - self.badgeNode = badgeNode - self.addSubnode(badgeNode) - } - badgeNode.text = badge - let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)) - } else if let badgeNode = self.badgeNode { - self.badgeNode = nil - badgeNode.removeFromSupernode() + if let badgeNode = self.badgeNode { + transition.updateFrame(node: badgeNode, frame: badgeFrame) } if self.subtitle != self.subtitleNode.attributedText?.string { diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index 05828dc801a..4327d8489a2 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -215,16 +215,14 @@ private enum StatsEntry: ItemListNodeEntry { } } -private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineStoryItem.Views?, messages: SearchMessagesResult?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { +private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineStoryItem.Views?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { var entries: [StatsEntry] = [] if let data = data { entries.append(.overviewTitle(presentationData.theme, presentationData.strings.Stats_MessageOverview.uppercased())) var publicShares: Int32? - if let messages { - publicShares = messages.totalCount - } else if let forwards { + if let forwards { publicShares = forwards.count } entries.append(.overview(presentationData.theme, data, storyViews, publicShares)) @@ -254,15 +252,6 @@ private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineS entries.append(.reactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.reactionsGraph, .bars, isStories)) } - if let messages, !messages.messages.isEmpty { - entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased())) - var index: Int32 = 0 - for message in messages.messages { - entries.append(.publicForward(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, .message(message))) - index += 1 - } - } - if let forwards, !forwards.forwards.isEmpty { entries.append(.publicForwardsTitle(presentationData.theme, presentationData.strings.Stats_MessagePublicForwardsTitle.uppercased())) var index: Int32 = 0 @@ -305,7 +294,6 @@ public func messageStatsController(context: AccountContext, updatedPresentationD let actionsDisposable = DisposableSet() let dataPromise = Promise(nil) - let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil) let forwardsPromise = Promise(nil) let anyStatsContext: Any @@ -314,7 +302,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD var openStoryImpl: ((EnginePeer.Id, EngineStoryItem, UIView) -> Void)? var storyContextActionImpl: ((EnginePeer.Id, ASDisplayNode, ContextGesture?, Bool) -> Void)? - var forwardsContext: StoryStatsPublicForwardsContext? + let forwardsContext: StoryStatsPublicForwardsContext let peerId: EnginePeer.Id var storyItem: EngineStoryItem? switch subject { @@ -331,19 +319,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD dataPromise.set(.single(nil) |> then(dataSignal)) anyStatsContext = statsContext - let searchSignal = context.engine.messages.searchMessages(location: .publicForwards(messageId: id), query: "", state: nil) - |> map(Optional.init) - |> afterNext { result in - if let result = result { - for message in result.0.messages { - if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) { - let _ = context.engine.peers.updatedRemotePeer(peer: peerReference).start() - } - } - } - } - messagesPromise.set(.single(nil) |> then(searchSignal)) - forwardsPromise.set(.single(nil)) + forwardsContext = StoryStatsPublicForwardsContext(account: context.account, subject: .message(messageId: id)) case let .story(peerIdValue, id, item, _): peerId = peerIdValue storyItem = item @@ -358,23 +334,18 @@ public func messageStatsController(context: AccountContext, updatedPresentationD } dataPromise.set(.single(nil) |> then(dataSignal)) anyStatsContext = statsContext - - messagesPromise.set(.single(nil)) - - forwardsContext = StoryStatsPublicForwardsContext(account: context.account, peerId: peerId, storyId: id) - if let forwardsContext { - forwardsPromise.set( - .single(nil) - |> then( - forwardsContext.state - |> map(Optional.init) - ) - ) - } else { - forwardsPromise.set(.single(nil)) - } + + forwardsContext = StoryStatsPublicForwardsContext(account: context.account, subject: .story(peerId: peerId, id: id)) } + forwardsPromise.set( + .single(nil) + |> then( + forwardsContext.state + |> map(Optional.init) + ) + ) + let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return loadDetailedGraphImpl?(graph, x) ?? .single(nil) }, openMessage: { messageId in @@ -412,14 +383,13 @@ public func messageStatsController(context: AccountContext, updatedPresentationD let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest( presentationData, - dataPromise.get(), - messagesPromise.get(), + dataPromise.get(), forwardsPromise.get(), longLoadingSignal, iconNodePromise.get() ) |> deliverOnMainQueue - |> map { presentationData, data, search, forwards, longLoading, iconNode -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, data, forwards, longLoading, iconNode -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) var emptyStateItem: ItemListControllerEmptyStateItem? if data == nil { @@ -445,7 +415,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD openStoryImpl?(peerId, storyItem, iconNode.view) } }) }, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, storyViews: storyViews, messages: search?.0, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, storyViews: storyViews, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -464,7 +434,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD }) } controller.visibleBottomContentOffsetChanged = { [weak forwardsContext] offset in - if case let .known(value) = offset, value < 100.0, case .story = subject { + if case let .known(value) = offset, value < 100.0 { forwardsContext?.loadMore() } } diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index acb8a5da3e2..a5f5e7d029c 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -464,7 +464,7 @@ class StatsOverviewItemNode: ListViewItemNode { nil ) - let hasMessages = stats.viewsPerPost.current > 0 + let hasMessages = stats.viewsPerPost.current > 0 || viewsPerPostDelta.hasValue let hasStories = stats.viewsPerStory.current > 0 || viewsPerStoryDelta.hasValue var items: [Int: (String, String, (String, ValueItemNode.DeltaColor)?)] = [:] @@ -483,7 +483,7 @@ class StatsOverviewItemNode: ListViewItemNode { (sharesPerPostDelta.text, sharesPerPostDelta.positive ? .positive : .negative) ) } - if stats.reactionsPerPost.current > 0 || reactionsPerStoryDelta.hasValue { + if stats.reactionsPerPost.current > 0 || reactionsPerPostDelta.hasValue { let index = hasStories ? 4 : 2 items[index] = ( compactNumericCountString(Int(stats.reactionsPerPost.current)), @@ -492,17 +492,20 @@ class StatsOverviewItemNode: ListViewItemNode { ) } if hasStories { - items[1] = ( + let index = items[0] == nil ? 0 : 1 + items[index] = ( compactNumericCountString(Int(stats.viewsPerStory.current)), item.presentationData.strings.Stats_ViewsPerStory, (viewsPerStoryDelta.text, viewsPerStoryDelta.positive ? .positive : .negative) ) - items[3] = ( + let sharesIndex = items[1] == nil ? 1 : 3 + items[sharesIndex] = ( compactNumericCountString(Int(stats.sharesPerStory.current)), item.presentationData.strings.Stats_SharesPerStory, (sharesPerStoryDelta.text, sharesPerStoryDelta.positive ? .positive : .negative) ) - items[5] = ( + let reactionsIndex = items[3] == nil ? 2 : 5 + items[reactionsIndex] = ( compactNumericCountString(Int(stats.reactionsPerStory.current)), item.presentationData.strings.Stats_ReactionsPerStory, (reactionsPerStoryDelta.text, reactionsPerStoryDelta.positive ? .positive : .negative) diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index 1ef5a35a389..f09cf1c31d5 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -1,9 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -NGDEPS = [ - "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", -] - swift_library( name = "StickerPackPreviewUI", module_name = "StickerPackPreviewUI", @@ -13,7 +9,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = NGDEPS + [ + deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 08e505d5551..0c966f3ef92 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -1,6 +1,3 @@ -// MARK: Nicegram StickerMaker -import FeatPartners -// import Foundation import UIKit import Display @@ -91,13 +88,6 @@ private enum StickerPackNextAction { } private final class StickerPackContainer: ASDisplayNode { - - // MARK: Nicegram StickerMaker - private let stickerMakerNode = ASDisplayNode { - StickerMakerButton(location: .stickerPack) - } - // - let index: Int private let context: AccountContext private weak var controller: StickerPackScreenImpl? @@ -256,10 +246,6 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.topContainerNode) self.topContainerNode.addSubnode(self.cancelButtonNode) self.topContainerNode.addSubnode(self.moreButtonNode) - - // MARK: Nicegram StickerMaker - self.addSubnode(self.stickerMakerNode) - // self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -1259,14 +1245,7 @@ private final class StickerPackContainer: ASDisplayNode { } let buttonSideInset: CGFloat = 16.0 - // MARK: Nicegram StickerMaker, change let to var - var titleAreaInset: CGFloat = 56.0 - - // MARK: Nicegram StickerMaker - if StickerMaker.showConfig.stickerPack { - titleAreaInset += StickerMakerButton.height - } - // + let titleAreaInset: CGFloat = 56.0 var actionAreaHeight: CGFloat = buttonHeight actionAreaHeight += insets.bottom + actionAreaBottomInset @@ -1407,21 +1386,6 @@ private final class StickerPackContainer: ASDisplayNode { transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) - // MARK: Nicegram StickerMaker - let stickerMakerSideInset = layout.safeInsets.left + 16 - transition.updateFrame( - node: self.stickerMakerNode, - frame: CGRect( - x: stickerMakerSideInset, - y: backgroundFrame.minY + 56, - width: layout.size.width - stickerMakerSideInset * 2, - height: StickerMakerButton.height - ) - ) - - self.stickerMakerNode.isHidden = !StickerMaker.showConfig.stickerPack - // - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: self.titleSeparatorNode, alpha: unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 212ceef5d5f..d3776d015d4 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -104,16 +104,18 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[531458253] = { return Api.ChannelAdminLogEvent.parse_channelAdminLogEvent($0) } dict[1427671598] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeAbout($0) } dict[-1102180616] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeAvailableReactions($0) } - dict[1147126836] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeBackgroundEmoji($0) } - dict[1009460347] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeColor($0) } + dict[1051328177] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeEmojiStatus($0) } dict[1855199800] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeHistoryTTL($0) } dict[84703944] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeLinkedChat($0) } dict[241923758] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeLocation($0) } + dict[1469507456] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangePeerColor($0) } dict[1129042607] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangePhoto($0) } + dict[1581742885] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeProfilePeerColor($0) } dict[-1312568665] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeStickerSet($0) } dict[-421545947] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeTitle($0) } dict[1783299128] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeUsername($0) } dict[-263212119] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeUsernames($0) } + dict[834362706] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeWallpaper($0) } dict[1483767080] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionCreateTopic($0) } dict[771095562] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionDefaultBannedRights($0) } dict[1121994683] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionDeleteMessage($0) } @@ -166,7 +168,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) } dict[-566281095] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsRecent($0) } dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) } - dict[-1903702824] = { return Api.Chat.parse_channel($0) } + dict[179174543] = { return Api.Chat.parse_channel($0) } dict[399807445] = { return Api.Chat.parse_channelForbidden($0) } dict[1103884886] = { return Api.Chat.parse_chat($0) } dict[693512293] = { return Api.Chat.parse_chatEmpty($0) } @@ -174,7 +176,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1605510357] = { return Api.ChatAdminRights.parse_chatAdminRights($0) } dict[-219353309] = { return Api.ChatAdminWithInvites.parse_chatAdminWithInvites($0) } dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } - dict[1915758525] = { return Api.ChatFull.parse_channelFull($0) } + dict[254528367] = { return Api.ChatFull.parse_channelFull($0) } dict[-908914376] = { return Api.ChatFull.parse_chatFull($0) } dict[-840897472] = { return Api.ChatInvite.parse_chatInvite($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } @@ -402,6 +404,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[42402760] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmoji($0) } dict[215889721] = { return Api.InputStickerSet.parse_inputStickerSetAnimatedEmojiAnimations($0) } dict[-427863538] = { return Api.InputStickerSet.parse_inputStickerSetDice($0) } + dict[1232373075] = { return Api.InputStickerSet.parse_inputStickerSetEmojiChannelDefaultStatuses($0) } dict[701560302] = { return Api.InputStickerSet.parse_inputStickerSetEmojiDefaultStatuses($0) } dict[1153562857] = { return Api.InputStickerSet.parse_inputStickerSetEmojiDefaultTopicIcons($0) } dict[80008398] = { return Api.InputStickerSet.parse_inputStickerSetEmojiGenericAnimations($0) } @@ -414,7 +417,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } - dict[2090038758] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } + dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } @@ -445,7 +448,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[901503851] = { return Api.KeyboardButton.parse_keyboardButtonCallback($0) } dict[1358175439] = { return Api.KeyboardButton.parse_keyboardButtonGame($0) } dict[-59151553] = { return Api.KeyboardButton.parse_keyboardButtonRequestGeoLocation($0) } - dict[218842764] = { return Api.KeyboardButton.parse_keyboardButtonRequestPeer($0) } + dict[1406648280] = { return Api.KeyboardButton.parse_keyboardButtonRequestPeer($0) } dict[-1318425559] = { return Api.KeyboardButton.parse_keyboardButtonRequestPhone($0) } dict[-1144565411] = { return Api.KeyboardButton.parse_keyboardButtonRequestPoll($0) } dict[-1598009252] = { return Api.KeyboardButton.parse_keyboardButtonSimpleWebView($0) } @@ -462,7 +465,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[695856818] = { return Api.LangPackString.parse_langPackStringDeleted($0) } dict[1816636575] = { return Api.LangPackString.parse_langPackStringPluralized($0) } dict[-1361650766] = { return Api.MaskCoords.parse_maskCoords($0) } + dict[577893055] = { return Api.MediaArea.parse_inputMediaAreaChannelPost($0) } dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) } + dict[1996756655] = { return Api.MediaArea.parse_mediaAreaChannelPost($0) } dict[-544523486] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } @@ -487,7 +492,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } - dict[-758129906] = { return Api.MessageAction.parse_messageActionGiftCode($0) } + dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) } dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[858499565] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[715107781] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) } @@ -499,7 +504,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1892568281] = { return Api.MessageAction.parse_messageActionPaymentSentMe($0) } dict[-2132731265] = { return Api.MessageAction.parse_messageActionPhoneCall($0) } dict[-1799538451] = { return Api.MessageAction.parse_messageActionPinMessage($0) } - dict[-25742243] = { return Api.MessageAction.parse_messageActionRequestedPeer($0) } + dict[827428507] = { return Api.MessageAction.parse_messageActionRequestedPeer($0) } dict[1200788123] = { return Api.MessageAction.parse_messageActionScreenshotTaken($0) } dict[-648257196] = { return Api.MessageAction.parse_messageActionSecureValuesSent($0) } dict[455635795] = { return Api.MessageAction.parse_messageActionSecureValuesSentMe($0) } @@ -542,7 +547,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) } dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) } dict[-1186937242] = { return Api.MessageMedia.parse_messageMediaGeoLive($0) } - dict[1478887012] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) } + dict[-626162256] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) } + dict[-963047320] = { return Api.MessageMedia.parse_messageMediaGiveawayResults($0) } dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) } dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) } dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) } @@ -825,7 +831,12 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1352440415] = { return Api.StoryItem.parse_storyItem($0) } dict[1374088783] = { return Api.StoryItem.parse_storyItemDeleted($0) } dict[-5388013] = { return Api.StoryItem.parse_storyItemSkipped($0) } + dict[1620104917] = { return Api.StoryReaction.parse_storyReaction($0) } + dict[-1146411453] = { return Api.StoryReaction.parse_storyReactionPublicForward($0) } + dict[-808644845] = { return Api.StoryReaction.parse_storyReactionPublicRepost($0) } dict[-1329730875] = { return Api.StoryView.parse_storyView($0) } + dict[-1870436597] = { return Api.StoryView.parse_storyViewPublicForward($0) } + dict[-1116418231] = { return Api.StoryView.parse_storyViewPublicRepost($0) } dict[-1923523370] = { return Api.StoryViews.parse_storyViews($0) } dict[1964978502] = { return Api.TextWithEntities.parse_textWithEntities($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } @@ -849,6 +860,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1232025500] = { return Api.Update.parse_updateBotInlineQuery($0) } dict[317794823] = { return Api.Update.parse_updateBotInlineSend($0) } dict[347625491] = { return Api.Update.parse_updateBotMenuButton($0) } + dict[-1407069234] = { return Api.Update.parse_updateBotMessageReaction($0) } + dict[164329305] = { return Api.Update.parse_updateBotMessageReactions($0) } dict[-1934976362] = { return Api.Update.parse_updateBotPrecheckoutQuery($0) } dict[-1246823043] = { return Api.Update.parse_updateBotShippingQuery($0) } dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } @@ -987,7 +1000,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[228623102] = { return Api.VideoSize.parse_videoSizeStickerMarkup($0) } dict[-1539849235] = { return Api.WallPaper.parse_wallPaper($0) } dict[-528465642] = { return Api.WallPaper.parse_wallPaperNoFile($0) } - dict[499236004] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } + dict[925826256] = { return Api.WallPaperSettings.parse_wallPaperSettings($0) } dict[-1493633966] = { return Api.WebAuthorization.parse_webAuthorization($0) } dict[475467473] = { return Api.WebDocument.parse_webDocument($0) } dict[-104284986] = { return Api.WebDocument.parse_webDocumentNoProxy($0) } @@ -1085,7 +1098,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) } dict[-1600596305] = { return Api.help.PassportConfig.parse_passportConfig($0) } dict[-1078332329] = { return Api.help.PassportConfig.parse_passportConfigNotModified($0) } - dict[324785199] = { return Api.help.PeerColorOption.parse_peerColorOption($0) } + dict[-276549461] = { return Api.help.PeerColorOption.parse_peerColorOption($0) } dict[1987928555] = { return Api.help.PeerColorSet.parse_peerColorProfileSet($0) } dict[639736408] = { return Api.help.PeerColorSet.parse_peerColorSet($0) } dict[16313608] = { return Api.help.PeerColors.parse_peerColors($0) } @@ -1173,7 +1186,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) } dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } - dict[-1222446760] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) } + dict[675942550] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } dict[1130879648] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) } dict[13456752] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) } @@ -1216,8 +1229,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1574486984] = { return Api.stories.Stories.parse_stories($0) } + dict[-1436583780] = { return Api.stories.StoryReactionsList.parse_storyReactionsList($0) } dict[-560009955] = { return Api.stories.StoryViews.parse_storyViews($0) } - dict[1189722604] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } + dict[1507299269] = { return Api.stories.StoryViewsList.parse_storyViewsList($0) } dict[543450958] = { return Api.updates.ChannelDifference.parse_channelDifference($0) } dict[1041346555] = { return Api.updates.ChannelDifference.parse_channelDifferenceEmpty($0) } dict[-1531132162] = { return Api.updates.ChannelDifference.parse_channelDifferenceTooLong($0) } @@ -1799,6 +1813,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StoryItem: _1.serialize(buffer, boxed) + case let _1 as Api.StoryReaction: + _1.serialize(buffer, boxed) case let _1 as Api.StoryView: _1.serialize(buffer, boxed) case let _1 as Api.StoryViews: @@ -2133,6 +2149,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.Stories: _1.serialize(buffer, boxed) + case let _1 as Api.stories.StoryReactionsList: + _1.serialize(buffer, boxed) case let _1 as Api.stories.StoryViews: _1.serialize(buffer, boxed) case let _1 as Api.stories.StoryViewsList: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 242e60ab2bf..1335731228f 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -325,6 +325,7 @@ public extension Api { case inputStickerSetAnimatedEmoji case inputStickerSetAnimatedEmojiAnimations case inputStickerSetDice(emoticon: String) + case inputStickerSetEmojiChannelDefaultStatuses case inputStickerSetEmojiDefaultStatuses case inputStickerSetEmojiDefaultTopicIcons case inputStickerSetEmojiGenericAnimations @@ -352,6 +353,12 @@ public extension Api { buffer.appendInt32(-427863538) } serializeString(emoticon, buffer: buffer, boxed: false) + break + case .inputStickerSetEmojiChannelDefaultStatuses: + if boxed { + buffer.appendInt32(1232373075) + } + break case .inputStickerSetEmojiDefaultStatuses: if boxed { @@ -407,6 +414,8 @@ public extension Api { return ("inputStickerSetAnimatedEmojiAnimations", []) case .inputStickerSetDice(let emoticon): return ("inputStickerSetDice", [("emoticon", emoticon as Any)]) + case .inputStickerSetEmojiChannelDefaultStatuses: + return ("inputStickerSetEmojiChannelDefaultStatuses", []) case .inputStickerSetEmojiDefaultStatuses: return ("inputStickerSetEmojiDefaultStatuses", []) case .inputStickerSetEmojiDefaultTopicIcons: @@ -441,6 +450,9 @@ public extension Api { return nil } } + public static func parse_inputStickerSetEmojiChannelDefaultStatuses(_ reader: BufferReader) -> InputStickerSet? { + return Api.InputStickerSet.inputStickerSetEmojiChannelDefaultStatuses + } public static func parse_inputStickerSetEmojiDefaultStatuses(_ reader: BufferReader) -> InputStickerSet? { return Api.InputStickerSet.inputStickerSetEmojiDefaultStatuses } @@ -604,7 +616,7 @@ public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64) - case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) + case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case inputStorePaymentPremiumSubscription(flags: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -631,9 +643,9 @@ public extension Api { serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) break - case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let randomId, let untilDate, let currency, let amount): + case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): if boxed { - buffer.appendInt32(2090038758) + buffer.appendInt32(369444042) } serializeInt32(flags, buffer: buffer, boxed: false) boostPeer.serialize(buffer, true) @@ -647,6 +659,7 @@ public extension Api { for item in countriesIso2! { serializeString(item, buffer: buffer, boxed: false) }} + if Int(flags) & Int(1 << 4) != 0 {serializeString(prizeDescription!, buffer: buffer, boxed: false)} serializeInt64(randomId, buffer: buffer, boxed: false) serializeInt32(untilDate, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -667,8 +680,8 @@ public extension Api { return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)]) - case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let randomId, let untilDate, let currency, let amount): - return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): + return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)]) } @@ -735,24 +748,27 @@ public extension Api { if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } - var _5: Int64? - _5 = reader.readInt64() - var _6: Int32? - _6 = reader.readInt32() - var _7: String? - _7 = parseString(reader) - var _8: Int64? - _8 = reader.readInt64() + var _5: String? + if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } + var _6: Int64? + _6 = reader.readInt64() + var _7: Int32? + _7 = reader.readInt32() + var _8: String? + _8 = parseString(reader) + var _9: Int64? + _9 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = _5 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, additionalPeers: _3, countriesIso2: _4, randomId: _5!, untilDate: _6!, currency: _7!, amount: _8!) + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, additionalPeers: _3, countriesIso2: _4, prizeDescription: _5, randomId: _6!, untilDate: _7!, currency: _8!, amount: _9!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index daac038500f..666ea7ff21c 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -429,7 +429,7 @@ public extension Api { case keyboardButtonCallback(flags: Int32, text: String, data: Buffer) case keyboardButtonGame(text: String) case keyboardButtonRequestGeoLocation(text: String) - case keyboardButtonRequestPeer(text: String, buttonId: Int32, peerType: Api.RequestPeerType) + case keyboardButtonRequestPeer(text: String, buttonId: Int32, peerType: Api.RequestPeerType, maxQuantity: Int32) case keyboardButtonRequestPhone(text: String) case keyboardButtonRequestPoll(flags: Int32, quiz: Api.Bool?, text: String) case keyboardButtonSimpleWebView(text: String, url: String) @@ -490,13 +490,14 @@ public extension Api { } serializeString(text, buffer: buffer, boxed: false) break - case .keyboardButtonRequestPeer(let text, let buttonId, let peerType): + case .keyboardButtonRequestPeer(let text, let buttonId, let peerType, let maxQuantity): if boxed { - buffer.appendInt32(218842764) + buffer.appendInt32(1406648280) } serializeString(text, buffer: buffer, boxed: false) serializeInt32(buttonId, buffer: buffer, boxed: false) peerType.serialize(buffer, true) + serializeInt32(maxQuantity, buffer: buffer, boxed: false) break case .keyboardButtonRequestPhone(let text): if boxed { @@ -582,8 +583,8 @@ public extension Api { return ("keyboardButtonGame", [("text", text as Any)]) case .keyboardButtonRequestGeoLocation(let text): return ("keyboardButtonRequestGeoLocation", [("text", text as Any)]) - case .keyboardButtonRequestPeer(let text, let buttonId, let peerType): - return ("keyboardButtonRequestPeer", [("text", text as Any), ("buttonId", buttonId as Any), ("peerType", peerType as Any)]) + case .keyboardButtonRequestPeer(let text, let buttonId, let peerType, let maxQuantity): + return ("keyboardButtonRequestPeer", [("text", text as Any), ("buttonId", buttonId as Any), ("peerType", peerType as Any), ("maxQuantity", maxQuantity as Any)]) case .keyboardButtonRequestPhone(let text): return ("keyboardButtonRequestPhone", [("text", text as Any)]) case .keyboardButtonRequestPoll(let flags, let quiz, let text): @@ -714,11 +715,14 @@ public extension Api { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.RequestPeerType } + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.KeyboardButton.keyboardButtonRequestPeer(text: _1!, buttonId: _2!, peerType: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.KeyboardButton.keyboardButtonRequestPeer(text: _1!, buttonId: _2!, peerType: _3!, maxQuantity: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 54a2a17dbac..d5155f12fbf 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -155,14 +155,24 @@ public extension Api { } } public extension Api { - enum MediaArea: TypeConstructorDescription { + indirect enum MediaArea: TypeConstructorDescription { + case inputMediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channel: Api.InputChannel, msgId: Int32) case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String) + case mediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channelId: Int64, msgId: Int32) case mediaAreaGeoPoint(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint) case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputMediaAreaChannelPost(let coordinates, let channel, let msgId): + if boxed { + buffer.appendInt32(577893055) + } + coordinates.serialize(buffer, true) + channel.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + break case .inputMediaAreaVenue(let coordinates, let queryId, let resultId): if boxed { buffer.appendInt32(-1300094593) @@ -171,6 +181,14 @@ public extension Api { serializeInt64(queryId, buffer: buffer, boxed: false) serializeString(resultId, buffer: buffer, boxed: false) break + case .mediaAreaChannelPost(let coordinates, let channelId, let msgId): + if boxed { + buffer.appendInt32(1996756655) + } + coordinates.serialize(buffer, true) + serializeInt64(channelId, buffer: buffer, boxed: false) + serializeInt32(msgId, buffer: buffer, boxed: false) + break case .mediaAreaGeoPoint(let coordinates, let geo): if boxed { buffer.appendInt32(-544523486) @@ -203,8 +221,12 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputMediaAreaChannelPost(let coordinates, let channel, let msgId): + return ("inputMediaAreaChannelPost", [("coordinates", coordinates as Any), ("channel", channel as Any), ("msgId", msgId as Any)]) case .inputMediaAreaVenue(let coordinates, let queryId, let resultId): return ("inputMediaAreaVenue", [("coordinates", coordinates as Any), ("queryId", queryId as Any), ("resultId", resultId as Any)]) + case .mediaAreaChannelPost(let coordinates, let channelId, let msgId): + return ("mediaAreaChannelPost", [("coordinates", coordinates as Any), ("channelId", channelId as Any), ("msgId", msgId as Any)]) case .mediaAreaGeoPoint(let coordinates, let geo): return ("mediaAreaGeoPoint", [("coordinates", coordinates as Any), ("geo", geo as Any)]) case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): @@ -214,6 +236,27 @@ public extension Api { } } + public static func parse_inputMediaAreaChannelPost(_ reader: BufferReader) -> MediaArea? { + var _1: Api.MediaAreaCoordinates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + } + var _2: Api.InputChannel? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputChannel + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MediaArea.inputMediaAreaChannelPost(coordinates: _1!, channel: _2!, msgId: _3!) + } + else { + return nil + } + } public static func parse_inputMediaAreaVenue(_ reader: BufferReader) -> MediaArea? { var _1: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { @@ -233,6 +276,25 @@ public extension Api { return nil } } + public static func parse_mediaAreaChannelPost(_ reader: BufferReader) -> MediaArea? { + var _1: Api.MediaAreaCoordinates? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + } + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MediaArea.mediaAreaChannelPost(coordinates: _1!, channelId: _2!, msgId: _3!) + } + else { + return nil + } + } public static func parse_mediaAreaGeoPoint(_ reader: BufferReader) -> MediaArea? { var _1: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { @@ -609,7 +671,7 @@ public extension Api { case messageActionEmpty case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) - case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String) + case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) case messageActionGiveawayLaunch case messageActionGiveawayResults(winnersCount: Int32, unclaimedCount: Int32) @@ -621,7 +683,7 @@ public extension Api { case messageActionPaymentSentMe(flags: Int32, currency: String, totalAmount: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, charge: Api.PaymentCharge) case messageActionPhoneCall(flags: Int32, callId: Int64, reason: Api.PhoneCallDiscardReason?, duration: Int32?) case messageActionPinMessage - case messageActionRequestedPeer(buttonId: Int32, peer: Api.Peer) + case messageActionRequestedPeer(buttonId: Int32, peers: [Api.Peer]) case messageActionScreenshotTaken case messageActionSecureValuesSent(types: [Api.SecureValueType]) case messageActionSecureValuesSentMe(values: [Api.SecureValue], credentials: Api.SecureCredentialsEncrypted) @@ -753,14 +815,18 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug): + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): if boxed { - buffer.appendInt32(-758129906) + buffer.appendInt32(1737240073) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)} serializeInt32(months, buffer: buffer, boxed: false) serializeString(slug, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeString(currency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} break case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): if boxed { @@ -854,12 +920,16 @@ public extension Api { } break - case .messageActionRequestedPeer(let buttonId, let peer): + case .messageActionRequestedPeer(let buttonId, let peers): if boxed { - buffer.appendInt32(-25742243) + buffer.appendInt32(827428507) } serializeInt32(buttonId, buffer: buffer, boxed: false) - peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } break case .messageActionScreenshotTaken: if boxed { @@ -986,8 +1056,8 @@ public extension Api { return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug): - return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any)]) + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): + return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) case .messageActionGiveawayLaunch: @@ -1010,8 +1080,8 @@ public extension Api { return ("messageActionPhoneCall", [("flags", flags as Any), ("callId", callId as Any), ("reason", reason as Any), ("duration", duration as Any)]) case .messageActionPinMessage: return ("messageActionPinMessage", []) - case .messageActionRequestedPeer(let buttonId, let peer): - return ("messageActionRequestedPeer", [("buttonId", buttonId as Any), ("peer", peer as Any)]) + case .messageActionRequestedPeer(let buttonId, let peers): + return ("messageActionRequestedPeer", [("buttonId", buttonId as Any), ("peers", peers as Any)]) case .messageActionScreenshotTaken: return ("messageActionScreenshotTaken", []) case .messageActionSecureValuesSent(let types): @@ -1236,12 +1306,24 @@ public extension Api { _3 = reader.readInt32() var _4: String? _4 = parseString(reader) + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: Int64? + if Int(_1!) & Int(1 << 2) != 0 {_6 = reader.readInt64() } + var _7: String? + if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) } + var _8: Int64? + if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!) + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8) } else { return nil @@ -1427,14 +1509,14 @@ public extension Api { public static func parse_messageActionRequestedPeer(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.Peer? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer + var _2: [Api.Peer]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } let _c1 = _1 != nil let _c2 = _2 != nil if _c1 && _c2 { - return Api.MessageAction.messageActionRequestedPeer(buttonId: _1!, peer: _2!) + return Api.MessageAction.messageActionRequestedPeer(buttonId: _1!, peers: _2!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 6ff56823602..faeac515b0a 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -697,7 +697,8 @@ public extension Api { case messageMediaGame(game: Api.Game) case messageMediaGeo(geo: Api.GeoPoint) case messageMediaGeoLive(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32, proximityNotificationRadius: Int32?) - case messageMediaGiveaway(flags: Int32, channels: [Int64], countriesIso2: [String]?, quantity: Int32, months: Int32, untilDate: Int32) + case messageMediaGiveaway(flags: Int32, channels: [Int64], countriesIso2: [String]?, prizeDescription: String?, quantity: Int32, months: Int32, untilDate: Int32) + case messageMediaGiveawayResults(flags: Int32, channelId: Int64, additionalPeersCount: Int32?, launchMsgId: Int32, winnersCount: Int32, unclaimedCount: Int32, winners: [Int64], months: Int32, prizeDescription: String?, untilDate: Int32) case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?) case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?) case messageMediaPoll(poll: Api.Poll, results: Api.PollResults) @@ -762,9 +763,9 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)} break - case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let quantity, let months, let untilDate): + case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let prizeDescription, let quantity, let months, let untilDate): if boxed { - buffer.appendInt32(1478887012) + buffer.appendInt32(-626162256) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -777,10 +778,30 @@ public extension Api { for item in countriesIso2! { serializeString(item, buffer: buffer, boxed: false) }} + if Int(flags) & Int(1 << 3) != 0 {serializeString(prizeDescription!, buffer: buffer, boxed: false)} serializeInt32(quantity, buffer: buffer, boxed: false) serializeInt32(months, buffer: buffer, boxed: false) serializeInt32(untilDate, buffer: buffer, boxed: false) break + case .messageMediaGiveawayResults(let flags, let channelId, let additionalPeersCount, let launchMsgId, let winnersCount, let unclaimedCount, let winners, let months, let prizeDescription, let untilDate): + if boxed { + buffer.appendInt32(-963047320) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(channelId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(additionalPeersCount!, buffer: buffer, boxed: false)} + serializeInt32(launchMsgId, buffer: buffer, boxed: false) + serializeInt32(winnersCount, buffer: buffer, boxed: false) + serializeInt32(unclaimedCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(winners.count)) + for item in winners { + serializeInt64(item, buffer: buffer, boxed: false) + } + serializeInt32(months, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(prizeDescription!, buffer: buffer, boxed: false)} + serializeInt32(untilDate, buffer: buffer, boxed: false) + break case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): if boxed { buffer.appendInt32(-156940077) @@ -862,8 +883,10 @@ public extension Api { return ("messageMediaGeo", [("geo", geo as Any)]) case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius): return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)]) - case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let quantity, let months, let untilDate): - return ("messageMediaGiveaway", [("flags", flags as Any), ("channels", channels as Any), ("countriesIso2", countriesIso2 as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)]) + case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let prizeDescription, let quantity, let months, let untilDate): + return ("messageMediaGiveaway", [("flags", flags as Any), ("channels", channels as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)]) + case .messageMediaGiveawayResults(let flags, let channelId, let additionalPeersCount, let launchMsgId, let winnersCount, let unclaimedCount, let winners, let months, let prizeDescription, let untilDate): + return ("messageMediaGiveawayResults", [("flags", flags as Any), ("channelId", channelId as Any), ("additionalPeersCount", additionalPeersCount as Any), ("launchMsgId", launchMsgId as Any), ("winnersCount", winnersCount as Any), ("unclaimedCount", unclaimedCount as Any), ("winners", winners as Any), ("months", months as Any), ("prizeDescription", prizeDescription as Any), ("untilDate", untilDate as Any)]) case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia): return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)]) case .messageMediaPhoto(let flags, let photo, let ttlSeconds): @@ -1007,20 +1030,63 @@ public extension Api { if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } + var _4: String? + if Int(_1!) & Int(1 << 3) != 0 {_4 = parseString(reader) } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, countriesIso2: _3, prizeDescription: _4, quantity: _5!, months: _6!, untilDate: _7!) + } + else { + return nil + } + } + public static func parse_messageMediaGiveawayResults(_ reader: BufferReader) -> MessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_3 = reader.readInt32() } var _4: Int32? _4 = reader.readInt32() var _5: Int32? _5 = reader.readInt32() var _6: Int32? _6 = reader.readInt32() + var _7: [Int64]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + var _8: Int32? + _8 = reader.readInt32() + var _9: String? + if Int(_1!) & Int(1 << 1) != 0 {_9 = parseString(reader) } + var _10: Int32? + _10 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, countriesIso2: _3, quantity: _4!, months: _5!, untilDate: _6!) + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + let _c10 = _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.MessageMedia.messageMediaGiveawayResults(flags: _1!, channelId: _2!, additionalPeersCount: _3, launchMsgId: _4!, winnersCount: _5!, unclaimedCount: _6!, winners: _7!, months: _8!, prizeDescription: _9, untilDate: _10!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 70febe0a838..e8bb5e4205f 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -724,16 +724,18 @@ public extension Api { indirect enum ChannelAdminLogEventAction: TypeConstructorDescription { case channelAdminLogEventActionChangeAbout(prevValue: String, newValue: String) case channelAdminLogEventActionChangeAvailableReactions(prevValue: Api.ChatReactions, newValue: Api.ChatReactions) - case channelAdminLogEventActionChangeBackgroundEmoji(prevValue: Int64, newValue: Int64) - case channelAdminLogEventActionChangeColor(prevValue: Int32, newValue: Int32) + case channelAdminLogEventActionChangeEmojiStatus(prevValue: Api.EmojiStatus, newValue: Api.EmojiStatus) case channelAdminLogEventActionChangeHistoryTTL(prevValue: Int32, newValue: Int32) case channelAdminLogEventActionChangeLinkedChat(prevValue: Int64, newValue: Int64) case channelAdminLogEventActionChangeLocation(prevValue: Api.ChannelLocation, newValue: Api.ChannelLocation) + case channelAdminLogEventActionChangePeerColor(prevValue: Api.PeerColor, newValue: Api.PeerColor) case channelAdminLogEventActionChangePhoto(prevPhoto: Api.Photo, newPhoto: Api.Photo) + case channelAdminLogEventActionChangeProfilePeerColor(prevValue: Api.PeerColor, newValue: Api.PeerColor) case channelAdminLogEventActionChangeStickerSet(prevStickerset: Api.InputStickerSet, newStickerset: Api.InputStickerSet) case channelAdminLogEventActionChangeTitle(prevValue: String, newValue: String) case channelAdminLogEventActionChangeUsername(prevValue: String, newValue: String) case channelAdminLogEventActionChangeUsernames(prevValue: [String], newValue: [String]) + case channelAdminLogEventActionChangeWallpaper(prevValue: Api.WallPaper, newValue: Api.WallPaper) case channelAdminLogEventActionCreateTopic(topic: Api.ForumTopic) case channelAdminLogEventActionDefaultBannedRights(prevBannedRights: Api.ChatBannedRights, newBannedRights: Api.ChatBannedRights) case channelAdminLogEventActionDeleteMessage(message: Api.Message) @@ -784,19 +786,12 @@ public extension Api { prevValue.serialize(buffer, true) newValue.serialize(buffer, true) break - case .channelAdminLogEventActionChangeBackgroundEmoji(let prevValue, let newValue): + case .channelAdminLogEventActionChangeEmojiStatus(let prevValue, let newValue): if boxed { - buffer.appendInt32(1147126836) + buffer.appendInt32(1051328177) } - serializeInt64(prevValue, buffer: buffer, boxed: false) - serializeInt64(newValue, buffer: buffer, boxed: false) - break - case .channelAdminLogEventActionChangeColor(let prevValue, let newValue): - if boxed { - buffer.appendInt32(1009460347) - } - serializeInt32(prevValue, buffer: buffer, boxed: false) - serializeInt32(newValue, buffer: buffer, boxed: false) + prevValue.serialize(buffer, true) + newValue.serialize(buffer, true) break case .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): if boxed { @@ -819,6 +814,13 @@ public extension Api { prevValue.serialize(buffer, true) newValue.serialize(buffer, true) break + case .channelAdminLogEventActionChangePeerColor(let prevValue, let newValue): + if boxed { + buffer.appendInt32(1469507456) + } + prevValue.serialize(buffer, true) + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionChangePhoto(let prevPhoto, let newPhoto): if boxed { buffer.appendInt32(1129042607) @@ -826,6 +828,13 @@ public extension Api { prevPhoto.serialize(buffer, true) newPhoto.serialize(buffer, true) break + case .channelAdminLogEventActionChangeProfilePeerColor(let prevValue, let newValue): + if boxed { + buffer.appendInt32(1581742885) + } + prevValue.serialize(buffer, true) + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionChangeStickerSet(let prevStickerset, let newStickerset): if boxed { buffer.appendInt32(-1312568665) @@ -862,6 +871,13 @@ public extension Api { serializeString(item, buffer: buffer, boxed: false) } break + case .channelAdminLogEventActionChangeWallpaper(let prevValue, let newValue): + if boxed { + buffer.appendInt32(834362706) + } + prevValue.serialize(buffer, true) + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionCreateTopic(let topic): if boxed { buffer.appendInt32(1483767080) @@ -1080,18 +1096,20 @@ public extension Api { return ("channelAdminLogEventActionChangeAbout", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeAvailableReactions(let prevValue, let newValue): return ("channelAdminLogEventActionChangeAvailableReactions", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) - case .channelAdminLogEventActionChangeBackgroundEmoji(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeBackgroundEmoji", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) - case .channelAdminLogEventActionChangeColor(let prevValue, let newValue): - return ("channelAdminLogEventActionChangeColor", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) + case .channelAdminLogEventActionChangeEmojiStatus(let prevValue, let newValue): + return ("channelAdminLogEventActionChangeEmojiStatus", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeHistoryTTL(let prevValue, let newValue): return ("channelAdminLogEventActionChangeHistoryTTL", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeLinkedChat(let prevValue, let newValue): return ("channelAdminLogEventActionChangeLinkedChat", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeLocation(let prevValue, let newValue): return ("channelAdminLogEventActionChangeLocation", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) + case .channelAdminLogEventActionChangePeerColor(let prevValue, let newValue): + return ("channelAdminLogEventActionChangePeerColor", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangePhoto(let prevPhoto, let newPhoto): return ("channelAdminLogEventActionChangePhoto", [("prevPhoto", prevPhoto as Any), ("newPhoto", newPhoto as Any)]) + case .channelAdminLogEventActionChangeProfilePeerColor(let prevValue, let newValue): + return ("channelAdminLogEventActionChangeProfilePeerColor", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeStickerSet(let prevStickerset, let newStickerset): return ("channelAdminLogEventActionChangeStickerSet", [("prevStickerset", prevStickerset as Any), ("newStickerset", newStickerset as Any)]) case .channelAdminLogEventActionChangeTitle(let prevValue, let newValue): @@ -1100,6 +1118,8 @@ public extension Api { return ("channelAdminLogEventActionChangeUsername", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionChangeUsernames(let prevValue, let newValue): return ("channelAdminLogEventActionChangeUsernames", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) + case .channelAdminLogEventActionChangeWallpaper(let prevValue, let newValue): + return ("channelAdminLogEventActionChangeWallpaper", [("prevValue", prevValue as Any), ("newValue", newValue as Any)]) case .channelAdminLogEventActionCreateTopic(let topic): return ("channelAdminLogEventActionCreateTopic", [("topic", topic as Any)]) case .channelAdminLogEventActionDefaultBannedRights(let prevBannedRights, let newBannedRights): @@ -1201,29 +1221,19 @@ public extension Api { return nil } } - public static func parse_channelAdminLogEventActionChangeBackgroundEmoji(_ reader: BufferReader) -> ChannelAdminLogEventAction? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeBackgroundEmoji(prevValue: _1!, newValue: _2!) + public static func parse_channelAdminLogEventActionChangeEmojiStatus(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.EmojiStatus? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.EmojiStatus } - else { - return nil + var _2: Api.EmojiStatus? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.EmojiStatus } - } - public static func parse_channelAdminLogEventActionChangeColor(_ reader: BufferReader) -> ChannelAdminLogEventAction? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil if _c1 && _c2 { - return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeColor(prevValue: _1!, newValue: _2!) + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeEmojiStatus(prevValue: _1!, newValue: _2!) } else { return nil @@ -1275,6 +1285,24 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionChangePeerColor(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.PeerColor? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerColor + } + var _2: Api.PeerColor? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PeerColor + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangePeerColor(prevValue: _1!, newValue: _2!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionChangePhoto(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.Photo? if let signature = reader.readInt32() { @@ -1293,6 +1321,24 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionChangeProfilePeerColor(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.PeerColor? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerColor + } + var _2: Api.PeerColor? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PeerColor + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeProfilePeerColor(prevValue: _1!, newValue: _2!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionChangeStickerSet(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.InputStickerSet? if let signature = reader.readInt32() { @@ -1357,6 +1403,24 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionChangeWallpaper(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.WallPaper? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.WallPaper + } + var _2: Api.WallPaper? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.WallPaper + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeWallpaper(prevValue: _1!, newValue: _2!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionCreateTopic(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.ForumTopic? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 82fa5e77566..53dc6c246e3 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -1191,161 +1191,95 @@ public extension Api { } } public extension Api { - enum StoryView: TypeConstructorDescription { - case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) + indirect enum StoryReaction: TypeConstructorDescription { + case storyReaction(peerId: Api.Peer, date: Int32, reaction: Api.Reaction) + case storyReactionPublicForward(message: Api.Message) + case storyReactionPublicRepost(peerId: Api.Peer, story: Api.StoryItem) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyView(let flags, let userId, let date, let reaction): + case .storyReaction(let peerId, let date, let reaction): if boxed { - buffer.appendInt32(-1329730875) + buffer.appendInt32(1620104917) } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) + peerId.serialize(buffer, true) serializeInt32(date, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} + reaction.serialize(buffer, true) + break + case .storyReactionPublicForward(let message): + if boxed { + buffer.appendInt32(-1146411453) + } + message.serialize(buffer, true) + break + case .storyReactionPublicRepost(let peerId, let story): + if boxed { + buffer.appendInt32(-808644845) + } + peerId.serialize(buffer, true) + story.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyView(let flags, let userId, let date, let reaction): - return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) + case .storyReaction(let peerId, let date, let reaction): + return ("storyReaction", [("peerId", peerId as Any), ("date", date as Any), ("reaction", reaction as Any)]) + case .storyReactionPublicForward(let message): + return ("storyReactionPublicForward", [("message", message as Any)]) + case .storyReactionPublicRepost(let peerId, let story): + return ("storyReactionPublicRepost", [("peerId", peerId as Any), ("story", story as Any)]) } } - public static func parse_storyView(_ reader: BufferReader) -> StoryView? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Api.Reaction? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Reaction - } } + public static func parse_storyReaction(_ reader: BufferReader) -> StoryReaction? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.Reaction? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Reaction + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) + if _c1 && _c2 && _c3 { + return Api.StoryReaction.storyReaction(peerId: _1!, date: _2!, reaction: _3!) } else { return nil } } - - } -} -public extension Api { - enum StoryViews: TypeConstructorDescription { - case storyViews(flags: Int32, viewsCount: Int32, forwardsCount: Int32?, reactions: [Api.ReactionCount]?, reactionsCount: Int32?, recentViewers: [Int64]?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): - if boxed { - buffer.appendInt32(-1923523370) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(viewsCount, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {serializeInt32(forwardsCount!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions!.count)) - for item in reactions! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(reactionsCount!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(recentViewers!.count)) - for item in recentViewers! { - serializeInt64(item, buffer: buffer, boxed: false) - }} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): - return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactions", reactions as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) - } - } - - public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } - var _4: [Api.ReactionCount]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) - } } - var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } - var _6: [Int64]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } } + public static func parse_storyReactionPublicForward(_ reader: BufferReader) -> StoryReaction? { + var _1: Api.Message? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Message + } let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6) + if _c1 { + return Api.StoryReaction.storyReactionPublicForward(message: _1!) } else { return nil } } - - } -} -public extension Api { - enum TextWithEntities: TypeConstructorDescription { - case textWithEntities(text: String, entities: [Api.MessageEntity]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .textWithEntities(let text, let entities): - if boxed { - buffer.appendInt32(1964978502) - } - serializeString(text, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities.count)) - for item in entities { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .textWithEntities(let text, let entities): - return ("textWithEntities", [("text", text as Any), ("entities", entities as Any)]) - } - } - - public static func parse_textWithEntities(_ reader: BufferReader) -> TextWithEntities? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + public static func parse_storyReactionPublicRepost(_ reader: BufferReader) -> StoryReaction? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Api.StoryItem? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StoryItem } let _c1 = _1 != nil let _c2 = _2 != nil if _c1 && _c2 { - return Api.TextWithEntities.textWithEntities(text: _1!, entities: _2!) + return Api.StoryReaction.storyReactionPublicRepost(peerId: _1!, story: _2!) } else { return nil @@ -1355,181 +1289,105 @@ public extension Api { } } public extension Api { - enum Theme: TypeConstructorDescription { - case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: [Api.ThemeSettings]?, emoticon: String?, installsCount: Int32?) + indirect enum StoryView: TypeConstructorDescription { + case storyView(flags: Int32, userId: Int64, date: Int32, reaction: Api.Reaction?) + case storyViewPublicForward(flags: Int32, message: Api.Message) + case storyViewPublicRepost(flags: Int32, peerId: Api.Peer, story: Api.StoryItem) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): + case .storyView(let flags, let userId, let date, let reaction): if boxed { - buffer.appendInt32(-1609668650) + buffer.appendInt32(-1329730875) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeString(slug, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(settings!.count)) - for item in settings! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 6) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(installsCount!, buffer: buffer, boxed: false)} + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {reaction!.serialize(buffer, true)} + break + case .storyViewPublicForward(let flags, let message): + if boxed { + buffer.appendInt32(-1870436597) + } + serializeInt32(flags, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + case .storyViewPublicRepost(let flags, let peerId, let story): + if boxed { + buffer.appendInt32(-1116418231) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peerId.serialize(buffer, true) + story.serialize(buffer, true) break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): - return ("theme", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("title", title as Any), ("document", document as Any), ("settings", settings as Any), ("emoticon", emoticon as Any), ("installsCount", installsCount as Any)]) + case .storyView(let flags, let userId, let date, let reaction): + return ("storyView", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("reaction", reaction as Any)]) + case .storyViewPublicForward(let flags, let message): + return ("storyViewPublicForward", [("flags", flags as Any), ("message", message as Any)]) + case .storyViewPublicRepost(let flags, let peerId, let story): + return ("storyViewPublicRepost", [("flags", flags as Any), ("peerId", peerId as Any), ("story", story as Any)]) } } - public static func parse_theme(_ reader: BufferReader) -> Theme? { + public static func parse_storyView(_ reader: BufferReader) -> StoryView? { var _1: Int32? _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Api.Document? + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Reaction? if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.Document - } } - var _7: [Api.ThemeSettings]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ThemeSettings.self) + _4 = Api.parse(reader, signature: signature) as? Api.Reaction } } - var _8: String? - if Int(_1!) & Int(1 << 6) != 0 {_8 = parseString(reader) } - var _9: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, emoticon: _8, installsCount: _9) + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryView.storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4) } else { return nil } } - - } -} -public extension Api { - enum ThemeSettings: TypeConstructorDescription { - case themeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.WallPaper?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): - if boxed { - buffer.appendInt32(-94849324) - } - serializeInt32(flags, buffer: buffer, boxed: false) - baseTheme.serialize(buffer, true) - serializeInt32(accentColor, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messageColors!.count)) - for item in messageColors! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): - return ("themeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any)]) - } - } - - public static func parse_themeSettings(_ reader: BufferReader) -> ThemeSettings? { + public static func parse_storyViewPublicForward(_ reader: BufferReader) -> StoryView? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.BaseTheme? + var _2: Api.Message? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + _2 = Api.parse(reader, signature: signature) as? Api.Message } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } - var _5: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - var _6: Api.WallPaper? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.WallPaper - } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.ThemeSettings.themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6) + if _c1 && _c2 { + return Api.StoryView.storyViewPublicForward(flags: _1!, message: _2!) } else { return nil } } - - } -} -public extension Api { - enum TopPeer: TypeConstructorDescription { - case topPeer(peer: Api.Peer, rating: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeer(let peer, let rating): - if boxed { - buffer.appendInt32(-305282981) - } - peer.serialize(buffer, true) - serializeDouble(rating, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .topPeer(let peer, let rating): - return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) - } - } - - public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { - var _1: Api.Peer? + public static func parse_storyViewPublicRepost(_ reader: BufferReader) -> StoryView? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: Api.StoryItem? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StoryItem } - var _2: Double? - _2 = reader.readDouble() let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.TopPeer.topPeer(peer: _1!, rating: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.StoryView.storyViewPublicRepost(flags: _1!, peerId: _2!, story: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index a6f81793c12..2ed34c1d5b3 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -1,3 +1,301 @@ +public extension Api { + enum StoryViews: TypeConstructorDescription { + case storyViews(flags: Int32, viewsCount: Int32, forwardsCount: Int32?, reactions: [Api.ReactionCount]?, reactionsCount: Int32?, recentViewers: [Int64]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): + if boxed { + buffer.appendInt32(-1923523370) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(viewsCount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(forwardsCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions!.count)) + for item in reactions! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(reactionsCount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(recentViewers!.count)) + for item in recentViewers! { + serializeInt64(item, buffer: buffer, boxed: false) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyViews(let flags, let viewsCount, let forwardsCount, let reactions, let reactionsCount, let recentViewers): + return ("storyViews", [("flags", flags as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactions", reactions as Any), ("reactionsCount", reactionsCount as Any), ("recentViewers", recentViewers as Any)]) + } + } + + public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt32() } + var _4: [Api.ReactionCount]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) + } } + var _5: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + var _6: [Int64]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.StoryViews.storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TextWithEntities: TypeConstructorDescription { + case textWithEntities(text: String, entities: [Api.MessageEntity]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .textWithEntities(let text, let entities): + if boxed { + buffer.appendInt32(1964978502) + } + serializeString(text, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities.count)) + for item in entities { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .textWithEntities(let text, let entities): + return ("textWithEntities", [("text", text as Any), ("entities", entities as Any)]) + } + } + + public static func parse_textWithEntities(_ reader: BufferReader) -> TextWithEntities? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TextWithEntities.textWithEntities(text: _1!, entities: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum Theme: TypeConstructorDescription { + case theme(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, document: Api.Document?, settings: [Api.ThemeSettings]?, emoticon: String?, installsCount: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): + if boxed { + buffer.appendInt32(-1609668650) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeString(slug, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {document!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(settings!.count)) + for item in settings! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 6) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(installsCount!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .theme(let flags, let id, let accessHash, let slug, let title, let document, let settings, let emoticon, let installsCount): + return ("theme", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("slug", slug as Any), ("title", title as Any), ("document", document as Any), ("settings", settings as Any), ("emoticon", emoticon as Any), ("installsCount", installsCount as Any)]) + } + } + + public static func parse_theme(_ reader: BufferReader) -> Theme? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.Document? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.Document + } } + var _7: [Api.ThemeSettings]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ThemeSettings.self) + } } + var _8: String? + if Int(_1!) & Int(1 << 6) != 0 {_8 = parseString(reader) } + var _9: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_9 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.Theme.theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, emoticon: _8, installsCount: _9) + } + else { + return nil + } + } + + } +} +public extension Api { + enum ThemeSettings: TypeConstructorDescription { + case themeSettings(flags: Int32, baseTheme: Api.BaseTheme, accentColor: Int32, outboxAccentColor: Int32?, messageColors: [Int32]?, wallpaper: Api.WallPaper?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): + if boxed { + buffer.appendInt32(-94849324) + } + serializeInt32(flags, buffer: buffer, boxed: false) + baseTheme.serialize(buffer, true) + serializeInt32(accentColor, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(outboxAccentColor!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messageColors!.count)) + for item in messageColors! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + if Int(flags) & Int(1 << 1) != 0 {wallpaper!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .themeSettings(let flags, let baseTheme, let accentColor, let outboxAccentColor, let messageColors, let wallpaper): + return ("themeSettings", [("flags", flags as Any), ("baseTheme", baseTheme as Any), ("accentColor", accentColor as Any), ("outboxAccentColor", outboxAccentColor as Any), ("messageColors", messageColors as Any), ("wallpaper", wallpaper as Any)]) + } + } + + public static func parse_themeSettings(_ reader: BufferReader) -> ThemeSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.BaseTheme? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BaseTheme + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_4 = reader.readInt32() } + var _5: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + var _6: Api.WallPaper? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.WallPaper + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.ThemeSettings.themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TopPeer: TypeConstructorDescription { + case topPeer(peer: Api.Peer, rating: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeer(let peer, let rating): + if boxed { + buffer.appendInt32(-305282981) + } + peer.serialize(buffer, true) + serializeDouble(rating, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .topPeer(let peer, let rating): + return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) + } + } + + public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TopPeer.topPeer(peer: _1!, rating: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum TopPeerCategory: TypeConstructorDescription { case topPeerCategoryBotsInline @@ -173,6 +471,8 @@ public extension Api { case updateBotInlineQuery(flags: Int32, queryId: Int64, userId: Int64, query: String, geo: Api.GeoPoint?, peerType: Api.InlineQueryPeerType?, offset: String) case updateBotInlineSend(flags: Int32, userId: Int64, query: String, geo: Api.GeoPoint?, id: String, msgId: Api.InputBotInlineMessageID?) case updateBotMenuButton(botId: Int64, button: Api.BotMenuButton) + case updateBotMessageReaction(peer: Api.Peer, msgId: Int32, date: Int32, actor: Api.Peer, oldReactions: [Api.Reaction], newReactions: [Api.Reaction], qts: Int32) + case updateBotMessageReactions(peer: Api.Peer, msgId: Int32, date: Int32, reactions: [Api.ReactionCount], qts: Int32) case updateBotPrecheckoutQuery(flags: Int32, queryId: Int64, userId: Int64, payload: Buffer, info: Api.PaymentRequestedInfo?, shippingOptionId: String?, currency: String, totalAmount: Int64) case updateBotShippingQuery(queryId: Int64, userId: Int64, payload: Buffer, shippingAddress: Api.PostAddress) case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) @@ -373,6 +673,40 @@ public extension Api { serializeInt64(botId, buffer: buffer, boxed: false) button.serialize(buffer, true) break + case .updateBotMessageReaction(let peer, let msgId, let date, let actor, let oldReactions, let newReactions, let qts): + if boxed { + buffer.appendInt32(-1407069234) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + actor.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(oldReactions.count)) + for item in oldReactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newReactions.count)) + for item in newReactions { + item.serialize(buffer, true) + } + serializeInt32(qts, buffer: buffer, boxed: false) + break + case .updateBotMessageReactions(let peer, let msgId, let date, let reactions, let qts): + if boxed { + buffer.appendInt32(164329305) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + serializeInt32(qts, buffer: buffer, boxed: false) + break case .updateBotPrecheckoutQuery(let flags, let queryId, let userId, let payload, let info, let shippingOptionId, let currency, let totalAmount): if boxed { buffer.appendInt32(-1934976362) @@ -1336,6 +1670,10 @@ public extension Api { return ("updateBotInlineSend", [("flags", flags as Any), ("userId", userId as Any), ("query", query as Any), ("geo", geo as Any), ("id", id as Any), ("msgId", msgId as Any)]) case .updateBotMenuButton(let botId, let button): return ("updateBotMenuButton", [("botId", botId as Any), ("button", button as Any)]) + case .updateBotMessageReaction(let peer, let msgId, let date, let actor, let oldReactions, let newReactions, let qts): + return ("updateBotMessageReaction", [("peer", peer as Any), ("msgId", msgId as Any), ("date", date as Any), ("actor", actor as Any), ("oldReactions", oldReactions as Any), ("newReactions", newReactions as Any), ("qts", qts as Any)]) + case .updateBotMessageReactions(let peer, let msgId, let date, let reactions, let qts): + return ("updateBotMessageReactions", [("peer", peer as Any), ("msgId", msgId as Any), ("date", date as Any), ("reactions", reactions as Any), ("qts", qts as Any)]) case .updateBotPrecheckoutQuery(let flags, let queryId, let userId, let payload, let info, let shippingOptionId, let currency, let totalAmount): return ("updateBotPrecheckoutQuery", [("flags", flags as Any), ("queryId", queryId as Any), ("userId", userId as Any), ("payload", payload as Any), ("info", info as Any), ("shippingOptionId", shippingOptionId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any)]) case .updateBotShippingQuery(let queryId, let userId, let payload, let shippingAddress): @@ -1752,6 +2090,70 @@ public extension Api { return nil } } + public static func parse_updateBotMessageReaction(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Peer? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _5: [Api.Reaction]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + var _6: [Api.Reaction]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + var _7: Int32? + _7 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.Update.updateBotMessageReaction(peer: _1!, msgId: _2!, date: _3!, actor: _4!, oldReactions: _5!, newReactions: _6!, qts: _7!) + } + else { + return nil + } + } + public static func parse_updateBotMessageReactions(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.ReactionCount]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) + } + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updateBotMessageReactions(peer: _1!, msgId: _2!, date: _3!, reactions: _4!, qts: _5!) + } + else { + return nil + } + } public static func parse_updateBotPrecheckoutQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index a098cfed273..ae4e12ba963 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -1156,13 +1156,13 @@ public extension Api { } public extension Api { enum WallPaperSettings: TypeConstructorDescription { - case wallPaperSettings(flags: Int32, backgroundColor: Int32?, secondBackgroundColor: Int32?, thirdBackgroundColor: Int32?, fourthBackgroundColor: Int32?, intensity: Int32?, rotation: Int32?) + case wallPaperSettings(flags: Int32, backgroundColor: Int32?, secondBackgroundColor: Int32?, thirdBackgroundColor: Int32?, fourthBackgroundColor: Int32?, intensity: Int32?, rotation: Int32?, emoticon: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation): + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation, let emoticon): if boxed { - buffer.appendInt32(499236004) + buffer.appendInt32(925826256) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(backgroundColor!, buffer: buffer, boxed: false)} @@ -1171,14 +1171,15 @@ public extension Api { if Int(flags) & Int(1 << 6) != 0 {serializeInt32(fourthBackgroundColor!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt32(intensity!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeInt32(rotation!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 7) != 0 {serializeString(emoticon!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation): - return ("wallPaperSettings", [("flags", flags as Any), ("backgroundColor", backgroundColor as Any), ("secondBackgroundColor", secondBackgroundColor as Any), ("thirdBackgroundColor", thirdBackgroundColor as Any), ("fourthBackgroundColor", fourthBackgroundColor as Any), ("intensity", intensity as Any), ("rotation", rotation as Any)]) + case .wallPaperSettings(let flags, let backgroundColor, let secondBackgroundColor, let thirdBackgroundColor, let fourthBackgroundColor, let intensity, let rotation, let emoticon): + return ("wallPaperSettings", [("flags", flags as Any), ("backgroundColor", backgroundColor as Any), ("secondBackgroundColor", secondBackgroundColor as Any), ("thirdBackgroundColor", thirdBackgroundColor as Any), ("fourthBackgroundColor", fourthBackgroundColor as Any), ("intensity", intensity as Any), ("rotation", rotation as Any), ("emoticon", emoticon as Any)]) } } @@ -1197,6 +1198,8 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {_6 = reader.readInt32() } var _7: Int32? if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } + var _8: String? + if Int(_1!) & Int(1 << 7) != 0 {_8 = parseString(reader) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil @@ -1204,8 +1207,9 @@ public extension Api { let _c5 = (Int(_1!) & Int(1 << 6) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.WallPaperSettings.wallPaperSettings(flags: _1!, backgroundColor: _2, secondBackgroundColor: _3, thirdBackgroundColor: _4, fourthBackgroundColor: _5, intensity: _6, rotation: _7) + let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.WallPaperSettings.wallPaperSettings(flags: _1!, backgroundColor: _2, secondBackgroundColor: _3, thirdBackgroundColor: _4, fourthBackgroundColor: _5, intensity: _6, rotation: _7, emoticon: _8) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 585befa4ad5..60f1674a87b 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1104,26 +1104,27 @@ public extension Api.help { } public extension Api.help { enum PeerColorOption: TypeConstructorDescription { - case peerColorOption(flags: Int32, colorId: Int32, colors: Api.help.PeerColorSet?, darkColors: Api.help.PeerColorSet?) + case peerColorOption(flags: Int32, colorId: Int32, colors: Api.help.PeerColorSet?, darkColors: Api.help.PeerColorSet?, channelMinLevel: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .peerColorOption(let flags, let colorId, let colors, let darkColors): + case .peerColorOption(let flags, let colorId, let colors, let darkColors, let channelMinLevel): if boxed { - buffer.appendInt32(324785199) + buffer.appendInt32(-276549461) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(colorId, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {colors!.serialize(buffer, true)} if Int(flags) & Int(1 << 2) != 0 {darkColors!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(channelMinLevel!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .peerColorOption(let flags, let colorId, let colors, let darkColors): - return ("peerColorOption", [("flags", flags as Any), ("colorId", colorId as Any), ("colors", colors as Any), ("darkColors", darkColors as Any)]) + case .peerColorOption(let flags, let colorId, let colors, let darkColors, let channelMinLevel): + return ("peerColorOption", [("flags", flags as Any), ("colorId", colorId as Any), ("colors", colors as Any), ("darkColors", darkColors as Any), ("channelMinLevel", channelMinLevel as Any)]) } } @@ -1140,12 +1141,15 @@ public extension Api.help { if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.help.PeerColorSet } } + var _5: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.help.PeerColorOption.peerColorOption(flags: _1!, colorId: _2!, colors: _3, darkColors: _4) + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.help.PeerColorOption.peerColorOption(flags: _1!, colorId: _2!, colors: _3, darkColors: _4, channelMinLevel: _5) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 40fbe24d593..0ff2e969750 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -792,16 +792,16 @@ public extension Api.payments { } public extension Api.payments { enum CheckedGiftCode: TypeConstructorDescription { - case checkedGiftCode(flags: Int32, fromId: Api.Peer, giveawayMsgId: Int32?, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User]) + case checkedGiftCode(flags: Int32, fromId: Api.Peer?, giveawayMsgId: Int32?, toId: Int64?, date: Int32, months: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .checkedGiftCode(let flags, let fromId, let giveawayMsgId, let toId, let date, let months, let usedDate, let chats, let users): if boxed { - buffer.appendInt32(-1222446760) + buffer.appendInt32(675942550) } serializeInt32(flags, buffer: buffer, boxed: false) - fromId.serialize(buffer, true) + if Int(flags) & Int(1 << 4) != 0 {fromId!.serialize(buffer, true)} if Int(flags) & Int(1 << 3) != 0 {serializeInt32(giveawayMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(toId!, buffer: buffer, boxed: false)} serializeInt32(date, buffer: buffer, boxed: false) @@ -832,9 +832,9 @@ public extension Api.payments { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer - } + } } var _3: Int32? if Int(_1!) & Int(1 << 3) != 0 {_3 = reader.readInt32() } var _4: Int64? @@ -854,7 +854,7 @@ public extension Api.payments { _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = _2 != nil + let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil @@ -863,7 +863,7 @@ public extension Api.payments { let _c8 = _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2!, giveawayMsgId: _3, toId: _4, date: _5!, months: _6!, usedDate: _7, chats: _8!, users: _9!) + return Api.payments.CheckedGiftCode.checkedGiftCode(flags: _1!, fromId: _2, giveawayMsgId: _3, toId: _4, date: _5!, months: _6!, usedDate: _7, chats: _8!, users: _9!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 1dbcfb84bdb..3baff622ff0 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -522,7 +522,7 @@ public extension Api { } public extension Api { indirect enum Chat: TypeConstructorDescription { - case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?) + case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, emojiStatus: Api.EmojiStatus?, level: Int32?) case channelForbidden(flags: Int32, id: Int64, accessHash: Int64, title: String, untilDate: Int32?) case chat(flags: Int32, id: Int64, title: String, photo: Api.ChatPhoto, participantsCount: Int32, date: Int32, version: Int32, migratedTo: Api.InputChannel?, adminRights: Api.ChatAdminRights?, defaultBannedRights: Api.ChatBannedRights?) case chatEmpty(id: Int64) @@ -530,9 +530,9 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color): + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level): if boxed { - buffer.appendInt32(-1903702824) + buffer.appendInt32(179174543) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -558,6 +558,9 @@ public extension Api { }} if Int(flags2) & Int(1 << 4) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)} if Int(flags2) & Int(1 << 7) != 0 {color!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 8) != 0 {profileColor!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 9) != 0 {emojiStatus!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 10) != 0 {serializeInt32(level!, buffer: buffer, boxed: false)} break case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): if boxed { @@ -602,8 +605,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color): - return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any)]) + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level): + return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("emojiStatus", emojiStatus as Any), ("level", level as Any)]) case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)]) case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights): @@ -662,6 +665,16 @@ public extension Api { if Int(_2!) & Int(1 << 7) != 0 {if let signature = reader.readInt32() { _16 = Api.parse(reader, signature: signature) as? Api.PeerColor } } + var _17: Api.PeerColor? + if Int(_2!) & Int(1 << 8) != 0 {if let signature = reader.readInt32() { + _17 = Api.parse(reader, signature: signature) as? Api.PeerColor + } } + var _18: Api.EmojiStatus? + if Int(_2!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _18 = Api.parse(reader, signature: signature) as? Api.EmojiStatus + } } + var _19: Int32? + if Int(_2!) & Int(1 << 10) != 0 {_19 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -678,8 +691,11 @@ public extension Api { let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil let _c15 = (Int(_2!) & Int(1 << 4) == 0) || _15 != nil let _c16 = (Int(_2!) & Int(1 << 7) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15, color: _16) + let _c17 = (Int(_2!) & Int(1 << 8) == 0) || _17 != nil + let _c18 = (Int(_2!) & Int(1 << 9) == 0) || _18 != nil + let _c19 = (Int(_2!) & Int(1 << 10) == 0) || _19 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { + return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15, color: _16, profileColor: _17, emojiStatus: _18, level: _19) } else { return nil @@ -904,14 +920,14 @@ public extension Api { } public extension Api { enum ChatFull: TypeConstructorDescription { - case channelFull(flags: Int32, flags2: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?, availableReactions: Api.ChatReactions?, stories: Api.PeerStories?) + case channelFull(flags: Int32, flags2: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?, availableReactions: Api.ChatReactions?, stories: Api.PeerStories?, wallpaper: Api.WallPaper?) case chatFull(flags: Int32, id: Int64, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?, ttlPeriod: Int32?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, availableReactions: Api.ChatReactions?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions, let stories): + case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions, let stories, let wallpaper): if boxed { - buffer.appendInt32(1915758525) + buffer.appendInt32(254528367) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -963,6 +979,7 @@ public extension Api { if Int(flags) & Int(1 << 29) != 0 {defaultSendAs!.serialize(buffer, true)} if Int(flags) & Int(1 << 30) != 0 {availableReactions!.serialize(buffer, true)} if Int(flags2) & Int(1 << 4) != 0 {stories!.serialize(buffer, true)} + if Int(flags2) & Int(1 << 7) != 0 {wallpaper!.serialize(buffer, true)} break case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): if boxed { @@ -999,8 +1016,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions, let stories): - return ("channelFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("participantsCount", participantsCount as Any), ("adminsCount", adminsCount as Any), ("kickedCount", kickedCount as Any), ("bannedCount", bannedCount as Any), ("onlineCount", onlineCount as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("chatPhoto", chatPhoto as Any), ("notifySettings", notifySettings as Any), ("exportedInvite", exportedInvite as Any), ("botInfo", botInfo as Any), ("migratedFromChatId", migratedFromChatId as Any), ("migratedFromMaxId", migratedFromMaxId as Any), ("pinnedMsgId", pinnedMsgId as Any), ("stickerset", stickerset as Any), ("availableMinId", availableMinId as Any), ("folderId", folderId as Any), ("linkedChatId", linkedChatId as Any), ("location", location as Any), ("slowmodeSeconds", slowmodeSeconds as Any), ("slowmodeNextSendDate", slowmodeNextSendDate as Any), ("statsDc", statsDc as Any), ("pts", pts as Any), ("call", call as Any), ("ttlPeriod", ttlPeriod as Any), ("pendingSuggestions", pendingSuggestions as Any), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs as Any), ("themeEmoticon", themeEmoticon as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any), ("defaultSendAs", defaultSendAs as Any), ("availableReactions", availableReactions as Any), ("stories", stories as Any)]) + case .channelFull(let flags, let flags2, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions, let stories, let wallpaper): + return ("channelFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("participantsCount", participantsCount as Any), ("adminsCount", adminsCount as Any), ("kickedCount", kickedCount as Any), ("bannedCount", bannedCount as Any), ("onlineCount", onlineCount as Any), ("readInboxMaxId", readInboxMaxId as Any), ("readOutboxMaxId", readOutboxMaxId as Any), ("unreadCount", unreadCount as Any), ("chatPhoto", chatPhoto as Any), ("notifySettings", notifySettings as Any), ("exportedInvite", exportedInvite as Any), ("botInfo", botInfo as Any), ("migratedFromChatId", migratedFromChatId as Any), ("migratedFromMaxId", migratedFromMaxId as Any), ("pinnedMsgId", pinnedMsgId as Any), ("stickerset", stickerset as Any), ("availableMinId", availableMinId as Any), ("folderId", folderId as Any), ("linkedChatId", linkedChatId as Any), ("location", location as Any), ("slowmodeSeconds", slowmodeSeconds as Any), ("slowmodeNextSendDate", slowmodeNextSendDate as Any), ("statsDc", statsDc as Any), ("pts", pts as Any), ("call", call as Any), ("ttlPeriod", ttlPeriod as Any), ("pendingSuggestions", pendingSuggestions as Any), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs as Any), ("themeEmoticon", themeEmoticon as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any), ("defaultSendAs", defaultSendAs as Any), ("availableReactions", availableReactions as Any), ("stories", stories as Any), ("wallpaper", wallpaper as Any)]) case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): return ("chatFull", [("flags", flags as Any), ("id", id as Any), ("about", about as Any), ("participants", participants as Any), ("chatPhoto", chatPhoto as Any), ("notifySettings", notifySettings as Any), ("exportedInvite", exportedInvite as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("folderId", folderId as Any), ("call", call as Any), ("ttlPeriod", ttlPeriod as Any), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs as Any), ("themeEmoticon", themeEmoticon as Any), ("requestsPending", requestsPending as Any), ("recentRequesters", recentRequesters as Any), ("availableReactions", availableReactions as Any)]) } @@ -1109,6 +1126,10 @@ public extension Api { if Int(_2!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { _38 = Api.parse(reader, signature: signature) as? Api.PeerStories } } + var _39: Api.WallPaper? + if Int(_2!) & Int(1 << 7) != 0 {if let signature = reader.readInt32() { + _39 = Api.parse(reader, signature: signature) as? Api.WallPaper + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1147,8 +1168,9 @@ public extension Api { let _c36 = (Int(_1!) & Int(1 << 29) == 0) || _36 != nil let _c37 = (Int(_1!) & Int(1 << 30) == 0) || _37 != nil let _c38 = (Int(_2!) & Int(1 << 4) == 0) || _38 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 && _c37 && _c38 { - return Api.ChatFull.channelFull(flags: _1!, flags2: _2!, id: _3!, about: _4!, participantsCount: _5, adminsCount: _6, kickedCount: _7, bannedCount: _8, onlineCount: _9, readInboxMaxId: _10!, readOutboxMaxId: _11!, unreadCount: _12!, chatPhoto: _13!, notifySettings: _14!, exportedInvite: _15, botInfo: _16!, migratedFromChatId: _17, migratedFromMaxId: _18, pinnedMsgId: _19, stickerset: _20, availableMinId: _21, folderId: _22, linkedChatId: _23, location: _24, slowmodeSeconds: _25, slowmodeNextSendDate: _26, statsDc: _27, pts: _28!, call: _29, ttlPeriod: _30, pendingSuggestions: _31, groupcallDefaultJoinAs: _32, themeEmoticon: _33, requestsPending: _34, recentRequesters: _35, defaultSendAs: _36, availableReactions: _37, stories: _38) + let _c39 = (Int(_2!) & Int(1 << 7) == 0) || _39 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 && _c37 && _c38 && _c39 { + return Api.ChatFull.channelFull(flags: _1!, flags2: _2!, id: _3!, about: _4!, participantsCount: _5, adminsCount: _6, kickedCount: _7, bannedCount: _8, onlineCount: _9, readInboxMaxId: _10!, readOutboxMaxId: _11!, unreadCount: _12!, chatPhoto: _13!, notifySettings: _14!, exportedInvite: _15, botInfo: _16!, migratedFromChatId: _17, migratedFromMaxId: _18, pinnedMsgId: _19, stickerset: _20, availableMinId: _21, folderId: _22, linkedChatId: _23, location: _24, slowmodeSeconds: _25, slowmodeNextSendDate: _26, statsDc: _27, pts: _28!, call: _29, ttlPeriod: _30, pendingSuggestions: _31, groupcallDefaultJoinAs: _32, themeEmoticon: _33, requestsPending: _34, recentRequesters: _35, defaultSendAs: _36, availableReactions: _37, stories: _38, wallpaper: _39) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index c88196f1820..0f744475ba3 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1421,18 +1421,25 @@ public extension Api.stories { } } public extension Api.stories { - enum StoryViews: TypeConstructorDescription { - case storyViews(views: [Api.StoryViews], users: [Api.User]) + enum StoryReactionsList: TypeConstructorDescription { + case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViews(let views, let users): + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): if boxed { - buffer.appendInt32(-560009955) + buffer.appendInt32(-1436583780) } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { item.serialize(buffer, true) } buffer.appendInt32(481674261) @@ -1440,30 +1447,45 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViews(let views, let users): - return ("storyViews", [("views", views as Any), ("users", users as Any)]) + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } - public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { - var _1: [Api.StoryViews]? + public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StoryReaction]? if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self) } - var _2: [Api.User]? + var _4: [Api.Chat]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) } else { return nil @@ -1473,18 +1495,15 @@ public extension Api.stories { } } public extension Api.stories { - enum StoryViewsList: TypeConstructorDescription { - case storyViewsList(flags: Int32, count: Int32, reactionsCount: Int32, views: [Api.StoryView], users: [Api.User], nextOffset: String?) + enum StoryViews: TypeConstructorDescription { + case storyViews(views: [Api.StoryViews], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): + case .storyViews(let views, let users): if boxed { - buffer.appendInt32(1189722604) + buffer.appendInt32(-560009955) } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - serializeInt32(reactionsCount, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(views.count)) for item in views { @@ -1495,43 +1514,30 @@ public extension Api.stories { for item in users { item.serialize(buffer, true) } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .storyViewsList(let flags, let count, let reactionsCount, let views, let users, let nextOffset): - return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + case .storyViews(let views, let users): + return ("storyViews", [("views", views as Any), ("users", users as Any)]) } } - public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.StoryView]? + public static func parse_storyViews(_ reader: BufferReader) -> StoryViews? { + var _1: [Api.StoryViews]? if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryViews.self) } - var _5: [Api.User]? + var _2: [Api.User]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, reactionsCount: _3!, views: _4!, users: _5!, nextOffset: _6) + if _c1 && _c2 { + return Api.stories.StoryViews.storyViews(views: _1!, users: _2!) } else { return nil @@ -1540,60 +1546,24 @@ public extension Api.stories { } } -public extension Api.updates { - indirect enum ChannelDifference: TypeConstructorDescription { - case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User]) - case channelDifferenceEmpty(flags: Int32, pts: Int32, timeout: Int32?) - case channelDifferenceTooLong(flags: Int32, timeout: Int32?, dialog: Api.Dialog, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) +public extension Api.stories { + enum StoryViewsList: TypeConstructorDescription { + case storyViewsList(flags: Int32, count: Int32, viewsCount: Int32, forwardsCount: Int32, reactionsCount: Int32, views: [Api.StoryView], chats: [Api.Chat], users: [Api.User], nextOffset: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): - if boxed { - buffer.appendInt32(543450958) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(pts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(newMessages.count)) - for item in newMessages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(otherUpdates.count)) - for item in otherUpdates { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .channelDifferenceEmpty(let flags, let pts, let timeout): + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): if boxed { - buffer.appendInt32(1041346555) + buffer.appendInt32(1507299269) } serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(pts, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - break - case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): - if boxed { - buffer.appendInt32(-1531132162) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} - dialog.serialize(buffer, true) + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt32(viewsCount, buffer: buffer, boxed: false) + serializeInt32(forwardsCount, buffer: buffer, boxed: false) + serializeInt32(reactionsCount, buffer: buffer, boxed: false) buffer.appendInt32(481674261) - buffer.appendInt32(Int32(messages.count)) - for item in messages { + buffer.appendInt32(Int32(views.count)) + for item in views { item.serialize(buffer, true) } buffer.appendInt32(481674261) @@ -1606,104 +1576,54 @@ public extension Api.updates { for item in users { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): - return ("channelDifference", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any), ("newMessages", newMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any)]) - case .channelDifferenceEmpty(let flags, let pts, let timeout): - return ("channelDifferenceEmpty", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any)]) - case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): - return ("channelDifferenceTooLong", [("flags", flags as Any), ("timeout", timeout as Any), ("dialog", dialog as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + case .storyViewsList(let flags, let count, let viewsCount, let forwardsCount, let reactionsCount, let views, let chats, let users, let nextOffset): + return ("storyViewsList", [("flags", flags as Any), ("count", count as Any), ("viewsCount", viewsCount as Any), ("forwardsCount", forwardsCount as Any), ("reactionsCount", reactionsCount as Any), ("views", views as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) } } - public static func parse_channelDifference(_ reader: BufferReader) -> ChannelDifference? { + public static func parse_storyViewsList(_ reader: BufferReader) -> StoryViewsList? { var _1: Int32? _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _5: [Api.Update]? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: [Api.StoryView]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryView.self) } - var _6: [Api.Chat]? + var _7: [Api.Chat]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _7: [Api.User]? + var _8: [Api.User]? if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _9: String? + if Int(_1!) & Int(1 << 0) != 0 {_9 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.updates.ChannelDifference.channelDifference(flags: _1!, pts: _2!, timeout: _3, newMessages: _4!, otherUpdates: _5!, chats: _6!, users: _7!) - } - else { - return nil - } - } - public static func parse_channelDifferenceEmpty(_ reader: BufferReader) -> ChannelDifference? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.updates.ChannelDifference.channelDifferenceEmpty(flags: _1!, pts: _2!, timeout: _3) - } - else { - return nil - } - } - public static func parse_channelDifferenceTooLong(_ reader: BufferReader) -> ChannelDifference? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() } - var _3: Api.Dialog? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Dialog - } - var _4: [Api.Message]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.updates.ChannelDifference.channelDifferenceTooLong(flags: _1!, timeout: _2, dialog: _3!, messages: _4!, chats: _5!, users: _6!) + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.stories.StoryViewsList.storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index f6d0dc14fb8..0cfe25da84a 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,175 @@ +public extension Api.updates { + indirect enum ChannelDifference: TypeConstructorDescription { + case channelDifference(flags: Int32, pts: Int32, timeout: Int32?, newMessages: [Api.Message], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User]) + case channelDifferenceEmpty(flags: Int32, pts: Int32, timeout: Int32?) + case channelDifferenceTooLong(flags: Int32, timeout: Int32?, dialog: Api.Dialog, messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): + if boxed { + buffer.appendInt32(543450958) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(pts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(newMessages.count)) + for item in newMessages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(otherUpdates.count)) + for item in otherUpdates { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .channelDifferenceEmpty(let flags, let pts, let timeout): + if boxed { + buffer.appendInt32(1041346555) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(pts, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + break + case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): + if boxed { + buffer.appendInt32(-1531132162) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(timeout!, buffer: buffer, boxed: false)} + dialog.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(messages.count)) + for item in messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .channelDifference(let flags, let pts, let timeout, let newMessages, let otherUpdates, let chats, let users): + return ("channelDifference", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any), ("newMessages", newMessages as Any), ("otherUpdates", otherUpdates as Any), ("chats", chats as Any), ("users", users as Any)]) + case .channelDifferenceEmpty(let flags, let pts, let timeout): + return ("channelDifferenceEmpty", [("flags", flags as Any), ("pts", pts as Any), ("timeout", timeout as Any)]) + case .channelDifferenceTooLong(let flags, let timeout, let dialog, let messages, let chats, let users): + return ("channelDifferenceTooLong", [("flags", flags as Any), ("timeout", timeout as Any), ("dialog", dialog as Any), ("messages", messages as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_channelDifference(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Update]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Update.self) + } + var _6: [Api.Chat]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _7: [Api.User]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.updates.ChannelDifference.channelDifference(flags: _1!, pts: _2!, timeout: _3, newMessages: _4!, otherUpdates: _5!, chats: _6!, users: _7!) + } + else { + return nil + } + } + public static func parse_channelDifferenceEmpty(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.updates.ChannelDifference.channelDifferenceEmpty(flags: _1!, pts: _2!, timeout: _3) + } + else { + return nil + } + } + public static func parse_channelDifferenceTooLong(_ reader: BufferReader) -> ChannelDifference? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() } + var _3: Api.Dialog? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Dialog + } + var _4: [Api.Message]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.updates.ChannelDifference.channelDifferenceTooLong(flags: _1!, timeout: _2, dialog: _3!, messages: _4!, chats: _5!, users: _6!) + } + else { + return nil + } + } + + } +} public extension Api.updates { enum Difference: TypeConstructorDescription { case difference(newMessages: [Api.Message], newEncryptedMessages: [Api.EncryptedMessage], otherUpdates: [Api.Update], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index fd921095daf..6e161a1f2bd 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -328,6 +328,36 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func getChannelDefaultEmojiStatuses(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1999087573) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getChannelDefaultEmojiStatuses", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.EmojiStatuses? in + let reader = BufferReader(buffer) + var result: Api.account.EmojiStatuses? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.account.EmojiStatuses + } + return result + }) + } +} +public extension Api.functions.account { + static func getChannelRestrictedStatusEmojis(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(900325589) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "account.getChannelRestrictedStatusEmojis", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.EmojiList? in + let reader = BufferReader(buffer) + var result: Api.EmojiList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.EmojiList + } + return result + }) + } +} public extension Api.functions.account { static func getChatThemes(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3050,12 +3080,12 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func updateColor(flags: Int32, channel: Api.InputChannel, color: Int32, backgroundEmojiId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func updateColor(flags: Int32, channel: Api.InputChannel, color: Int32?, backgroundEmojiId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1645879327) + buffer.appendInt32(-659933583) serializeInt32(flags, buffer: buffer, boxed: false) channel.serialize(buffer, true) - serializeInt32(color, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(color!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(backgroundEmojiId!, buffer: buffer, boxed: false)} return (FunctionDescription(name: "channels.updateColor", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("color", String(describing: color)), ("backgroundEmojiId", String(describing: backgroundEmojiId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) @@ -3067,6 +3097,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func updateEmojiStatus(channel: Api.InputChannel, emojiStatus: Api.EmojiStatus) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-254548312) + channel.serialize(buffer, true) + emojiStatus.serialize(buffer, true) + return (FunctionDescription(name: "channels.updateEmojiStatus", parameters: [("channel", String(describing: channel)), ("emojiStatus", String(describing: emojiStatus))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.channels { static func updatePinnedForumTopic(channel: Api.InputChannel, topicId: Int32, pinned: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3795,21 +3841,6 @@ public extension Api.functions.help { }) } } -public extension Api.functions.help { - static func getAppChangelog(prevAppVersion: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1877938321) - serializeString(prevAppVersion, buffer: buffer, boxed: false) - return (FunctionDescription(name: "help.getAppChangelog", parameters: [("prevAppVersion", String(describing: prevAppVersion))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in - let reader = BufferReader(buffer) - var result: Api.Updates? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Updates - } - return result - }) - } -} public extension Api.functions.help { static func getAppConfig(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6734,14 +6765,18 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func sendBotRequestedPeer(peer: Api.InputPeer, msgId: Int32, buttonId: Int32, requestedPeer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func sendBotRequestedPeer(peer: Api.InputPeer, msgId: Int32, buttonId: Int32, requestedPeers: [Api.InputPeer]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-29831141) + buffer.appendInt32(-1850552224) peer.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) serializeInt32(buttonId, buffer: buffer, boxed: false) - requestedPeer.serialize(buffer, true) - return (FunctionDescription(name: "messages.sendBotRequestedPeer", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("buttonId", String(describing: buttonId)), ("requestedPeer", String(describing: requestedPeer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(requestedPeers.count)) + for item in requestedPeers { + item.serialize(buffer, true) + } + return (FunctionDescription(name: "messages.sendBotRequestedPeer", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("buttonId", String(describing: buttonId)), ("requestedPeers", String(describing: requestedPeers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -8572,20 +8607,18 @@ public extension Api.functions.stats { } } public extension Api.functions.stats { - static func getMessagePublicForwards(channel: Api.InputChannel, msgId: Int32, offsetRate: Int32, offsetPeer: Api.InputPeer, offsetId: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getMessagePublicForwards(channel: Api.InputChannel, msgId: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1445996571) + buffer.appendInt32(1595212100) channel.serialize(buffer, true) serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt32(offsetRate, buffer: buffer, boxed: false) - offsetPeer.serialize(buffer, true) - serializeInt32(offsetId, buffer: buffer, boxed: false) + serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stats.getMessagePublicForwards", parameters: [("channel", String(describing: channel)), ("msgId", String(describing: msgId)), ("offsetRate", String(describing: offsetRate)), ("offsetPeer", String(describing: offsetPeer)), ("offsetId", String(describing: offsetId)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + return (FunctionDescription(name: "stats.getMessagePublicForwards", parameters: [("channel", String(describing: channel)), ("msgId", String(describing: msgId)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.PublicForwards? in let reader = BufferReader(buffer) - var result: Api.messages.Messages? + var result: Api.stats.PublicForwards? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.messages.Messages + result = Api.parse(reader, signature: signature) as? Api.stats.PublicForwards } return result }) @@ -9084,6 +9117,26 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func getStoryReactionsList(flags: Int32, peer: Api.InputPeer, id: Int32, reaction: Api.Reaction?, offset: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1179482081) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {reaction!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(offset!, buffer: buffer, boxed: false)} + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.getStoryReactionsList", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("id", String(describing: id)), ("reaction", String(describing: reaction)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.StoryReactionsList? in + let reader = BufferReader(buffer) + var result: Api.stories.StoryReactionsList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stories.StoryReactionsList + } + return result + }) + } +} public extension Api.functions.stories { static func getStoryViewsList(flags: Int32, peer: Api.InputPeer, q: String?, id: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index 45d2b6e15fc..7b1431d82fe 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -333,10 +333,10 @@ public final class ManagedAudioSession: NSObject { var headphonesAreActive = false loop: for currentOutput in audioSession.currentRoute.outputs { switch currentOutput.portType { - case .headphones, .bluetoothA2DP, .bluetoothHFP: + case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE: headphonesAreActive = true hasHeadphones = true - hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP].contains(currentOutput.portType) + hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP, .bluetoothLE].contains(currentOutput.portType) activeOutput = .headphones break loop default: @@ -730,7 +730,7 @@ public final class ManagedAudioSession: NSObject { let route = AVAudioSession.sharedInstance().currentRoute //managedAudioSessionLog("\(route)") for desc in route.outputs { - if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP { + if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP || desc.portType == .bluetoothLE { return true } } @@ -977,7 +977,7 @@ public final class ManagedAudioSession: NSObject { } else { loop: for route in routes { switch route.portType { - case .headphones, .bluetoothA2DP, .bluetoothHFP: + case .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE: let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route) alreadySet = true break loop diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 0e096d91bb2..e39e7167327 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -50,6 +50,9 @@ public final class CallController: ViewController { return self._ready } + private let isDataReady = Promise(false) + private let isContentsReady = Promise(false) + private let sharedContext: SharedAccountContext private let account: Account public let call: PresentationCall @@ -73,6 +76,8 @@ public final class CallController: ViewController { private let idleTimerExtensionDisposable = MetaDisposable() + public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? + public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { self.sharedContext = sharedContext self.account = account @@ -83,6 +88,14 @@ public final class CallController: ViewController { super.init(navigationBarPresentationData: nil) + self._ready.set(combineLatest(queue: .mainQueue(), self.isDataReady.get(), self.isContentsReady.get()) + |> map { a, b -> Bool in + return a && b + } + |> filter { $0 } + |> take(1) + |> timeout(2.0, queue: .mainQueue(), alternate: .single(true))) + self.isOpaqueWhenInOverlay = true self.statusBar.statusBarStyle = .White @@ -135,10 +148,26 @@ public final class CallController: ViewController { } override public func loadDisplayNode() { - if self.sharedContext.immediateExperimentalUISettings.callUIV2 { - self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) - } else { + var useV2 = self.call.context.sharedContext.immediateExperimentalUISettings.callV2 + if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_callui_v2"] { + useV2 = false + } + + if !useV2 { self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call) + self.isContentsReady.set(.single(true)) + } else { + let displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call) + self.displayNode = displayNode + self.isContentsReady.set(displayNode.isReady.get()) + + displayNode.restoreUIForPictureInPicture = { [weak self] completion in + guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else { + completion(false) + return + } + restoreUIForPictureInPicture(completion) + } } self.displayNodeDidLoad() @@ -299,8 +328,11 @@ public final class CallController: ViewController { } self.controllerNode.dismissedInteractively = { [weak self] in - self?.didPlayPresentationAnimation = false - self?.presentingViewController?.dismiss(animated: false, completion: nil) + guard let self else { + return + } + self.didPlayPresentationAnimation = false + self.presentingViewController?.dismiss(animated: false, completion: nil) } self.peerDisposable = (combineLatest(self.account.postbox.peerView(id: self.account.peerId) |> take(1), self.account.postbox.peerView(id: self.call.peerId), self.sharedContext.activeAccountsWithInfo |> take(1)) @@ -310,7 +342,7 @@ public final class CallController: ViewController { strongSelf.peer = peer // MARK: Nicegram DB Changes strongSelf.controllerNode.updatePeer(accountPeer: accountPeer, peer: peer, hasOther: activeAccountsWithInfo.accounts.filter { !$0.account.isHidden }.count > 1) - strongSelf._ready.set(.single(true)) + strongSelf.isDataReady.set(.single(true)) } } }) diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index b2a2990e355..ee3c3f164cf 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -17,6 +17,7 @@ import AlertUI import PresentationDataUtils import DeviceAccess import ContextUI +import AppBundle private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) @@ -27,6 +28,8 @@ private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat } final class CallVideoNode: ASDisplayNode, PreviewVideoNode { + private var placeholderImageNode: ASImageNode? + private let videoTransformContainer: ASDisplayNode private let videoView: PresentationCallVideoView @@ -52,7 +55,7 @@ final class CallVideoNode: ASDisplayNode, PreviewVideoNode { private var previousVideoHeight: CGFloat? - init(videoView: PresentationCallVideoView, disabledText: String?, assumeReadyAfterTimeout: Bool, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping (CallVideoNode) -> Void) { + init(videoView: PresentationCallVideoView, displayPlaceholderUntilReady: Bool = false, disabledText: String?, assumeReadyAfterTimeout: Bool, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping (CallVideoNode) -> Void) { self.isReadyUpdated = isReadyUpdated self.isFlippedUpdated = isFlippedUpdated @@ -80,6 +83,13 @@ final class CallVideoNode: ASDisplayNode, PreviewVideoNode { self.videoTransformContainer.view.addSubview(self.videoView.view) self.addSubnode(self.videoTransformContainer) + if displayPlaceholderUntilReady { + let placeholderImageNode = ASImageNode() + placeholderImageNode.image = UIImage(bundleImageName: "Camera/SelfiePlaceholder") + self.placeholderImageNode = placeholderImageNode + self.addSubnode(placeholderImageNode) + } + if let disabledText = disabledText { self.videoPausedNode.attributedText = NSAttributedString(string: disabledText, font: Font.regular(17.0), textColor: .white) self.addSubnode(self.videoPausedNode) @@ -95,6 +105,13 @@ final class CallVideoNode: ASDisplayNode, PreviewVideoNode { strongSelf.readyPromise.set(true) strongSelf.isReadyTimer?.invalidate() strongSelf.isReadyUpdated() + + if let placeholderImageNode = strongSelf.placeholderImageNode { + strongSelf.placeholderImageNode = nil + placeholderImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak placeholderImageNode] _ in + placeholderImageNode?.removeFromSupernode() + }) + } } } } @@ -191,6 +208,11 @@ final class CallVideoNode: ASDisplayNode, PreviewVideoNode { func updateLayout(size: CGSize, cornerRadius: CGFloat, isOutgoing: Bool, deviceOrientation: UIDeviceOrientation, isCompactLayout: Bool, transition: ContainedViewLayoutTransition) { self.currentCornerRadius = cornerRadius + if let placeholderImageNode = self.placeholderImageNode, let image = placeholderImageNode.image { + let placeholderSize = image.size.aspectFilled(size) + transition.updateFrame(node: placeholderImageNode, frame: CGRect(origin: CGPoint(x: (size.width - placeholderSize.width) * 0.5, y: (size.height - placeholderSize.height) * 0.5), size: placeholderSize)) + } + var rotationAngle: CGFloat if false && isOutgoing && isCompactLayout { rotationAngle = CGFloat.pi / 2.0 diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 53d8dbbbb5e..89dd7adb28a 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -17,8 +17,17 @@ import ImageBlur import TelegramVoip import MetalEngine import DeviceAccess +import LibYuvBinding final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol { + private struct PanGestureState { + var offsetFraction: CGFloat + + init(offsetFraction: CGFloat) { + self.offsetFraction = offsetFraction + } + } + private let sharedContext: SharedAccountContext private let account: Account private let presentationData: PresentationData @@ -29,7 +38,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private let callScreen: PrivateCallScreen private var callScreenState: PrivateCallScreen.State? - private var shouldStayHiddenUntilConnection: Bool = false + let isReady = Promise() + private var didInitializeIsReady: Bool = false private var callStartTimestamp: Double? @@ -47,6 +57,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP var callEnded: ((Bool) -> Void)? var dismissedInteractively: (() -> Void)? var dismissAllTooltips: (() -> Void)? + var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? private var emojiKey: (data: Data, resolvedKey: [String])? private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? @@ -55,19 +66,23 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP private var peerAvatarDisposable: Disposable? private var availableAudioOutputs: [AudioSessionOutput]? + private var currentAudioOutput: AudioSessionOutput? private var isMicrophoneMutedDisposable: Disposable? private var audioLevelDisposable: Disposable? + private var audioOutputCheckTimer: Foundation.Timer? private var localVideo: AdaptedCallVideoSource? private var remoteVideo: AdaptedCallVideoSource? + private var panGestureState: PanGestureState? + private var notifyDismissedInteractivelyOnPanGestureApply: Bool = false + init( sharedContext: SharedAccountContext, account: Account, presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, - shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall ) { @@ -78,10 +93,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.call = call self.containerView = UIView() + self.containerView.clipsToBounds = true self.callScreen = PrivateCallScreen() - self.shouldStayHiddenUntilConnection = shouldStayHiddenUntilConnection - super.init() self.view.addSubview(self.containerView) @@ -122,6 +136,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } self.back?() + self.callScreen.beginPictureInPictureIfPossible() + } + self.callScreen.closeAction = { [weak self] in + guard let self else { + return + } + self.dismissedInteractively?() + } + self.callScreen.restoreUIForPictureInPicture = { [weak self] completion in + guard let self else { + completion(false) + return + } + self.restoreUIForPictureInPicture?(completion) } self.callScreenState = PrivateCallScreen.State( @@ -130,7 +158,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP shortName: " ", avatarImage: nil, audioOutput: .internalSpeaker, - isMicrophoneMuted: false, + isLocalAudioMuted: false, + isRemoteAudioMuted: false, localVideo: nil, remoteVideo: nil, isRemoteBatteryLow: false @@ -145,8 +174,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } self.isMuted = isMuted - if callScreenState.isMicrophoneMuted != isMuted { - callScreenState.isMicrophoneMuted = isMuted + if callScreenState.isLocalAudioMuted != isMuted { + callScreenState.isLocalAudioMuted = isMuted self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.3, curve: .spring)) } @@ -159,16 +188,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } self.callScreen.addIncomingAudioLevel(value: audioLevel) }) + + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } deinit { self.peerAvatarDisposable?.dispose() self.isMicrophoneMutedDisposable?.dispose() self.audioLevelDisposable?.dispose() + self.audioOutputCheckTimer?.invalidate() } func updateAudioOutputs(availableOutputs: [AudioSessionOutput], currentOutput: AudioSessionOutput?) { self.availableAudioOutputs = availableOutputs + self.currentAudioOutput = currentOutput if var callScreenState = self.callScreenState { let mappedOutput: PrivateCallScreen.State.AudioOutput @@ -189,6 +222,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.audioOutput = mappedOutput self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.3, curve: .spring)) + + self.setupAudioOutputForVideoIfNeeded() } } } @@ -242,7 +277,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP var updateLayoutImpl: ((ContainerViewLayout, CGFloat) -> Void)? - let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, disabledText: nil, assumeReadyAfterTimeout: true, isReadyUpdated: { [weak self] in + let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, displayPlaceholderUntilReady: true, disabledText: nil, assumeReadyAfterTimeout: true, isReadyUpdated: { [weak self] in guard let self, let (layout, navigationBarHeight) = self.validLayout else { return } @@ -295,21 +330,23 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP let mappedLifecycleState: PrivateCallScreen.State.LifecycleState switch callState.state { case .waiting: - mappedLifecycleState = .connecting + mappedLifecycleState = .requesting case .ringing: mappedLifecycleState = .ringing case let .requesting(isRinging): if isRinging { mappedLifecycleState = .ringing } else { - mappedLifecycleState = .connecting + mappedLifecycleState = .requesting } - case let .connecting(keyData): - let _ = keyData - mappedLifecycleState = .exchangingKeys + case .connecting: + mappedLifecycleState = .connecting case let .active(startTime, signalQuality, keyData): self.callStartTimestamp = startTime + var signalQuality = signalQuality + signalQuality = 4 + let _ = keyData mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( startTime: startTime + kCFAbsoluteTimeIntervalSince1970, @@ -317,20 +354,51 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP emojiKey: self.resolvedEmojiKey(data: keyData) )) case let .reconnecting(startTime, _, keyData): - let _ = keyData - mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( - startTime: startTime + kCFAbsoluteTimeIntervalSince1970, - signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0), - emojiKey: self.resolvedEmojiKey(data: keyData) - )) - case .terminating, .terminated: + if self.callStartTimestamp != nil { + mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState( + startTime: startTime + kCFAbsoluteTimeIntervalSince1970, + signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0), + emojiKey: self.resolvedEmojiKey(data: keyData) + )) + } else { + mappedLifecycleState = .connecting + } + case .terminating(let reason), .terminated(_, let reason, _): let duration: Double if let callStartTimestamp = self.callStartTimestamp { duration = CFAbsoluteTimeGetCurrent() - callStartTimestamp } else { duration = 0.0 } - mappedLifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: duration)) + + let mappedReason: PrivateCallScreen.State.TerminatedState.Reason + if let reason { + switch reason { + case let .ended(type): + switch type { + case .missed: + if self.call.isOutgoing { + mappedReason = .hangUp + } else { + mappedReason = .missed + } + case .busy: + mappedReason = .busy + case .hungUp: + if self.callStartTimestamp != nil { + mappedReason = .hangUp + } else { + mappedReason = .declined + } + } + case .error: + mappedReason = .failed + } + } else { + mappedReason = .hangUp + } + + mappedLifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: duration, reason: mappedReason)) } switch callState.state { @@ -373,15 +441,69 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP callScreenState.isRemoteBatteryLow = false } + switch callState.remoteAudioState { + case .muted: + callScreenState.isRemoteAudioMuted = true + case .active: + callScreenState.isRemoteAudioMuted = false + } + if self.callScreenState != callScreenState { self.callScreenState = callScreenState self.update(transition: .animated(duration: 0.35, curve: .spring)) } + + self.setupAudioOutputForVideoIfNeeded() } if case let .terminated(_, _, reportRating) = callState.state { self.callEnded?(reportRating) } + + if !self.didInitializeIsReady { + self.didInitializeIsReady = true + + if let localVideo = self.localVideo { + self.isReady.set(Signal { subscriber in + return localVideo.addOnUpdated { + subscriber.putNext(true) + subscriber.putCompletion() + } + }) + } else { + self.isReady.set(.single(true)) + } + } + } + + private func setupAudioOutputForVideoIfNeeded() { + guard let callScreenState = self.callScreenState, let currentAudioOutput = self.currentAudioOutput else { + return + } + if callScreenState.localVideo != nil || callScreenState.remoteVideo != nil { + switch currentAudioOutput { + case .headphones, .speaker: + break + case let .port(port) where port.type == .bluetooth || port.type == .wired: + break + default: + self.setCurrentAudioOutput?(.speaker) + } + + if self.audioOutputCheckTimer == nil { + self.audioOutputCheckTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.setupAudioOutputForVideoIfNeeded() + }) + } + } else { + if let audioOutputCheckTimer = self.audioOutputCheckTimer { + self.audioOutputCheckTimer = nil + audioOutputCheckTimer.invalidate() + } + } } func updatePeer(accountPeer: Peer, peer: Peer, hasOther: Bool) { @@ -393,6 +515,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP return } callScreenState.name = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + callScreenState.shortName = peer.compactDisplayTitle if self.currentPeer?.smallProfileImage != peer.smallProfileImage { self.peerAvatarDisposable?.dispose() @@ -450,6 +573,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } func animateIn() { + self.panGestureState = nil + self.update(transition: .immediate) + if !self.containerView.alpha.isZero { var bounds = self.bounds bounds.origin = CGPoint() @@ -460,16 +586,20 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.containerView.layer.removeAnimation(forKey: "scale") self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - if !self.shouldStayHiddenUntilConnection { - self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) - self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } + self.containerView.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) + self.containerView.layer.allowsGroupOpacity = true + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in + guard let self else { + return + } + self.containerView.layer.allowsGroupOpacity = false + }) } } func animateOut(completion: @escaping () -> Void) { self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - if !self.shouldStayHiddenUntilConnection || self.containerView.alpha > 0.0 { + if self.containerView.alpha > 0.0 { self.containerView.layer.allowsGroupOpacity = true self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in self?.containerView.layer.allowsGroupOpacity = false @@ -483,7 +613,35 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } func expandFromPipIfPossible() { - + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began, .changed: + if !self.bounds.height.isZero && !self.notifyDismissedInteractivelyOnPanGestureApply { + let translation = recognizer.translation(in: self.view) + self.panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) + self.update(transition: .immediate) + } + case .cancelled, .ended: + if !self.bounds.height.isZero { + let translation = recognizer.translation(in: self.view) + let panGestureState = PanGestureState(offsetFraction: translation.y / self.bounds.height) + + let velocity = recognizer.velocity(in: self.view) + + self.panGestureState = nil + if abs(panGestureState.offsetFraction) > 0.6 || abs(velocity.y) >= 100.0 { + self.panGestureState = PanGestureState(offsetFraction: panGestureState.offsetFraction < 0.0 ? -1.0 : 1.0) + self.notifyDismissedInteractivelyOnPanGestureApply = true + self.callScreen.beginPictureInPictureIfPossible() + } + + self.update(transition: .animated(duration: 0.4, curve: .spring)) + } + default: + break + } } private func update(transition: ContainedViewLayoutTransition) { @@ -496,13 +654,38 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight) - transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: layout.size)) + var containerOffset: CGFloat = 0.0 + if let panGestureState = self.panGestureState { + containerOffset = panGestureState.offsetFraction * layout.size.height + self.containerView.layer.cornerRadius = layout.deviceMetrics.screenCornerRadius + } + + transition.updateFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: layout.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + if self.panGestureState == nil { + self.containerView.layer.cornerRadius = 0.0 + } + if self.notifyDismissedInteractivelyOnPanGestureApply { + self.notifyDismissedInteractivelyOnPanGestureApply = false + self.dismissedInteractively?() + } + }) transition.updateFrame(view: self.callScreen, frame: CGRect(origin: CGPoint(), size: layout.size)) - if let callScreenState = self.callScreenState { + if var callScreenState = self.callScreenState { + if case .terminated = callScreenState.lifecycleState { + callScreenState.isLocalAudioMuted = false + callScreenState.isRemoteAudioMuted = false + callScreenState.isRemoteBatteryLow = false + callScreenState.localVideo = nil + callScreenState.remoteVideo = nil + } self.callScreen.update( size: layout.size, insets: layout.insets(options: [.statusBar]), + interfaceOrientation: layout.metrics.orientation ?? .portrait, screenCornerRadius: layout.deviceMetrics.screenCornerRadius, state: callScreenState, transition: Transition(transition) @@ -511,7 +694,100 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP } } +private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool { + guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else { + return false + } + guard CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) == buffer.width else { + return false + } + guard CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) == buffer.height else { + return false + } + + let cvRet = CVPixelBufferLockBaseAddress(pixelBuffer, []) + if cvRet != kCVReturnSuccess { + return false + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, []) + } + + guard let dstY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return false + } + let dstStrideY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + + guard let dstUV = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return false + } + let dstStrideUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + + buffer.y.withUnsafeBytes { srcYBuffer in + guard let srcY = srcYBuffer.baseAddress else { + return + } + buffer.u.withUnsafeBytes { srcUBuffer in + guard let srcU = srcUBuffer.baseAddress else { + return + } + buffer.v.withUnsafeBytes { srcVBuffer in + guard let srcV = srcVBuffer.baseAddress else { + return + } + libyuv_I420ToNV12( + srcY.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideY), + srcU.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideU), + srcV.assumingMemoryBound(to: UInt8.self), + Int32(buffer.strideV), + dstY.assumingMemoryBound(to: UInt8.self), + Int32(dstStrideY), + dstUV.assumingMemoryBound(to: UInt8.self), + Int32(dstStrideUV), + Int32(buffer.width), + Int32(buffer.height) + ) + } + } + } + + return true +} + private final class AdaptedCallVideoSource: VideoSource { + final class I420DataBuffer: Output.DataBuffer { + private let buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer + + override var pixelBuffer: CVPixelBuffer? { + let ioSurfaceProperties = NSMutableDictionary() + let options = NSMutableDictionary() + options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) + + var pixelBuffer: CVPixelBuffer? + CVPixelBufferCreate( + kCFAllocatorDefault, + self.buffer.width, + self.buffer.height, + kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + options, + &pixelBuffer + ) + if let pixelBuffer, copyI420BufferToNV12Buffer(buffer: buffer, pixelBuffer: pixelBuffer) { + return pixelBuffer + } else { + return nil + } + } + + init(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer) { + self.buffer = buffer + + super.init() + } + } + private static let queue = Queue(name: "AdaptedCallVideoSource") private var onUpdatedListeners = Bag<() -> Void>() private(set) var currentOutput: Output? @@ -540,6 +816,8 @@ private final class AdaptedCallVideoSource: VideoSource { rotationAngle = Float.pi * 3.0 / 2.0 } + let followsDeviceOrientation = videoFrameData.deviceRelativeOrientation != nil + var mirrorDirection: Output.MirrorDirection = [] var sourceId: Int = 0 @@ -599,14 +877,47 @@ private final class AdaptedCallVideoSource: VideoSource { return } + output = Output( + resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), + textureLayout: .biPlanar(Output.BiPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), + rotationAngle: rotationAngle, + followsDeviceOrientation: followsDeviceOrientation, + mirrorDirection: mirrorDirection, + sourceId: sourceId + ) + case let .i420(i420Buffer): + let width = i420Buffer.width + let height = i420Buffer.height + + let _ = width + let _ = height + return + + /*var cvMetalTextureY: CVMetalTexture? + var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY) + guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else { + return + } + var cvMetalTextureUV: CVMetalTexture? + status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV) + guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else { + return + } + output = Output( resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), y: yTexture, uv: uvTexture, + dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer), rotationAngle: rotationAngle, + followsDeviceOrientation: followsDeviceOrientation, mirrorDirection: mirrorDirection, sourceId: sourceId - ) + )*/ default: return } diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 45a2f734eae..014180aa627 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -10,12 +10,8 @@ import AccountContext import TelegramAudio import TelegramVoip -private let sharedProviderDelegate: AnyObject? = { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - return CallKitProviderDelegate() - } else { - return nil - } +private let sharedProviderDelegate: CallKitProviderDelegate? = { + return CallKitProviderDelegate() }() public final class CallKitIntegration { @@ -53,69 +49,50 @@ public final class CallKitIntegration { setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void ) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue) - } + sharedProviderDelegate?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged, hasActiveCallsValue: hasActiveCallsValue) } private init?() { if !CallKitIntegration.isAvailable { return nil } - - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - } else { - return nil - } } func startCall(context: AccountContext, peerId: EnginePeer.Id, phoneNumber: String?, localContactId: String?, isVideo: Bool, displayTitle: String) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle) - self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId) - } + sharedProviderDelegate?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle) + self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId) } func answerCall(uuid: UUID) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.answerCall(uuid: uuid) - } + sharedProviderDelegate?.answerCall(uuid: uuid) } public func dropCall(uuid: UUID) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.dropCall(uuid: uuid) - } + sharedProviderDelegate?.dropCall(uuid: uuid) } public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion) - } + sharedProviderDelegate?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion) } func reportOutgoingCallConnected(uuid: UUID, at date: Date) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.reportOutgoingCallConnected(uuid: uuid, at: date) - } + sharedProviderDelegate?.reportOutgoingCallConnected(uuid: uuid, at: date) } private func donateIntent(peerId: EnginePeer.Id, displayTitle: String, localContactId: String?) { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown) - let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())") + let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown) + let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())") + + let intent = INStartAudioCallIntent(destinationType: .normal, contacts: [contact]) - let intent = INStartAudioCallIntent(destinationType: .normal, contacts: [contact]) - - let interaction = INInteraction(intent: intent, response: nil) - interaction.direction = .outgoing - interaction.donate { _ in - } + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .outgoing + interaction.donate { _ in } } public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { - (sharedProviderDelegate as? CallKitProviderDelegate)?.applyVoiceChatOutputMode(outputMode: outputMode) + sharedProviderDelegate?.applyVoiceChatOutputMode(outputMode: outputMode) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 553f2bf350e..a70300baeeb 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -721,6 +721,7 @@ public final class MediaStreamComponent: CombinedComponent { var infoItem: AnyComponent? if let originInfo = context.state.originInfo { infoItem = AnyComponent(OriginInfoComponent( + strings: environment.strings, memberCount: originInfo.memberCount )) } @@ -933,6 +934,7 @@ public final class MediaStreamComponent: CombinedComponent { let sheet = sheet.update( component: StreamSheetComponent( + strings: environment.strings, topOffset: topOffset, sheetHeight: sheetHeight, backgroundColor: (isFullscreen && !state.hasVideo) ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), @@ -1691,15 +1693,21 @@ private final class StreamTitleComponent: Component { private final class OriginInfoComponent: CombinedComponent { + let strings: PresentationStrings let participantsCount: Int init( + strings: PresentationStrings, memberCount: Int ) { + self.strings = strings self.participantsCount = memberCount } static func ==(lhs: OriginInfoComponent, rhs: OriginInfoComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } if lhs.participantsCount != rhs.participantsCount { return false } @@ -1713,6 +1721,7 @@ private final class OriginInfoComponent: CombinedComponent { return { context in let viewerCounter = viewerCounter.update( component: ParticipantsComponent( + strings: context.component.strings, count: context.component.participantsCount, showsSubtitle: true, fontSize: 18.0, diff --git a/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift index a2aefbe5cb3..29e1b9d25e1 100644 --- a/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/ParticipantsComponent.swift @@ -9,12 +9,14 @@ private let purple = UIColor(rgb: 0x3252ef) private let pink = UIColor(rgb: 0xe4436c) final class ParticipantsComponent: Component { + private let strings: PresentationStrings private let count: Int private let showsSubtitle: Bool private let fontSize: CGFloat private let gradientColors: [CGColor] - init(count: Int, showsSubtitle: Bool = true, fontSize: CGFloat = 48.0, gradientColors: [CGColor] = [pink.cgColor, purple.cgColor, purple.cgColor]) { + init(strings: PresentationStrings, count: Int, showsSubtitle: Bool = true, fontSize: CGFloat = 48.0, gradientColors: [CGColor] = [pink.cgColor, purple.cgColor, purple.cgColor]) { + self.strings = strings self.count = count self.showsSubtitle = showsSubtitle self.fontSize = fontSize @@ -22,6 +24,9 @@ final class ParticipantsComponent: Component { } static func == (lhs: ParticipantsComponent, rhs: ParticipantsComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } if lhs.count != rhs.count { return false } @@ -41,8 +46,7 @@ final class ParticipantsComponent: Component { func update(view: View, availableSize: CGSize, state: ComponentFlow.EmptyComponentState, environment: ComponentFlow.Environment, transition: ComponentFlow.Transition) -> CGSize { view.counter.update( countString: self.count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "", - // TODO: localize - subtitle: self.showsSubtitle ? (self.count > 0 ? /*environment.strings.LiveStream_Watching*/"watching" : /*environment.strings.LiveStream_NoViewers.lowercased()*/"no viewers") : "", + subtitle: self.showsSubtitle ? (self.count > 0 ? self.strings.LiveStream_Watching.lowercased() : self.strings.LiveStream_NoViewers.lowercased()) : "", fontSize: self.fontSize, gradientColors: self.gradientColors ) diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index c9735c39674..54b037d457b 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -6,8 +6,10 @@ import AccountContext import AVKit import MultilineTextComponent import Display +import TelegramPresentationData final class StreamSheetComponent: CombinedComponent { + let strings: PresentationStrings let sheetHeight: CGFloat let topOffset: CGFloat let backgroundColor: UIColor @@ -22,6 +24,7 @@ final class StreamSheetComponent: CombinedComponent { let fullscreenBottomComponent: AnyComponent init( + strings: PresentationStrings, topOffset: CGFloat, sheetHeight: CGFloat, backgroundColor: UIColor, @@ -34,6 +37,7 @@ final class StreamSheetComponent: CombinedComponent { fullscreenTopComponent: AnyComponent, fullscreenBottomComponent: AnyComponent ) { + self.strings = strings self.topOffset = topOffset self.sheetHeight = sheetHeight self.backgroundColor = backgroundColor @@ -49,6 +53,9 @@ final class StreamSheetComponent: CombinedComponent { } static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } if lhs.topOffset != rhs.topOffset { return false } @@ -159,7 +166,7 @@ final class StreamSheetComponent: CombinedComponent { ) let viewerCounter = viewerCounter.update( - component: ParticipantsComponent(count: context.component.participantsCount, fontSize: 44.0), + component: ParticipantsComponent(strings: context.component.strings, count: context.component.participantsCount, fontSize: 44.0), availableSize: CGSize(width: context.availableSize.width, height: 70), transition: context.transition ) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 031a9353b74..9218f305eae 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -342,6 +342,32 @@ public final class PresentationCallImpl: PresentationCall { } private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) { + self.reception = reception + + if let ongoingContext = self.ongoingContext { + if self.receptionDisposable == nil, case .active = sessionState.state { + self.reception = 4 + + var canUpdate = false + self.receptionDisposable = (ongoingContext.reception + |> delay(1.0, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] reception in + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + if canUpdate { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl) + } else { + strongSelf.reception = reception + } + } else { + strongSelf.reception = reception + } + } + }) + canUpdate = true + } + } + if case .video = sessionState.type { self.isVideo = true } @@ -349,9 +375,10 @@ public final class PresentationCallImpl: PresentationCall { let previousControl = self.audioSessionControl self.sessionState = sessionState self.callContextState = callContextState - self.reception = reception self.audioSessionControl = audioSessionControl + let reception = self.reception + if previousControl != nil && audioSessionControl == nil { print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)") } @@ -559,17 +586,6 @@ public final class PresentationCallImpl: PresentationCall { } }) - self.receptionDisposable = (ongoingContext.reception - |> deliverOnMainQueue).start(next: { [weak self] reception in - if let strongSelf = self { - if let sessionState = strongSelf.sessionState { - strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl) - } else { - strongSelf.reception = reception - } - } - }) - self.audioLevelDisposable = (ongoingContext.audioLevel |> deliverOnMainQueue).start(next: { [weak self] level in if let strongSelf = self { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 1ed491fde8b..3ab4562186f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -839,7 +839,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if item.peer.isFake { credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = item.peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 086807c5a6a..310b8b36de9 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -469,7 +469,7 @@ struct AccountMutableState { for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): if let participantsCount = participantsCount { self.addOperation(.UpdateCachedPeerData(chat.peerId, { current in var previous: CachedChannelData diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index fd04895bfa9..2c95e1eac39 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -287,6 +287,7 @@ private var declaredEncodables: Void = { declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) }) declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) }) + declareEncodable(TelegramMediaGiveawayResults.self, f: { TelegramMediaGiveawayResults(decoder: $0) }) declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) }) declareEncodable(DerivedDataMessageAttribute.self, f: { DerivedDataMessageAttribute(decoder: $0) }) return diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 09181a7096e..a288585d959 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) - case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -153,17 +153,27 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - var nameColor: Int32? + var nameColorIndex: Int32? var backgroundEmojiId: Int64? if let color = color { switch color { case let .peerColor(_, color, backgroundEmojiIdValue): - nameColor = color + nameColorIndex = color backgroundEmojiId = backgroundEmojiIdValue } } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColor.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil) + var profileColorIndex: Int32? + var profileBackgroundEmojiId: Int64? + if let profileColor = profileColor { + switch profileColor { + case let .peerColor(_, color, backgroundEmojiIdValue): + profileColorIndex = color + profileBackgroundEmojiId = backgroundEmojiIdValue + } + } + + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -172,7 +182,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil) } } @@ -180,7 +190,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { switch rhs { case .chat, .chatEmpty, .chatForbidden, .channelForbidden: return parseTelegramGroupOrChannel(chat: rhs) - case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) @@ -232,7 +242,19 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { } } - return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil) + var profileColorIndex: Int32? + var profileBackgroundEmojiId: Int64? + if let profileColor = profileColor { + switch profileColor { + case let .peerColor(_, color, backgroundEmojiIdValue): + profileColorIndex = color + profileBackgroundEmojiId = backgroundEmojiIdValue + } + } + + let parsedEmojiStatus = emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)) + + return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel) } else { return parseTelegramGroupOrChannel(chat: rhs) } @@ -286,6 +308,6 @@ func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChanne let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden - return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId) + return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift index 00f0da59161..1b289cd0aa1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReplyMarkupMessageAttribute.swift @@ -63,7 +63,7 @@ extension ReplyMarkupButton { self.init(title: text, titleWhenForwarded: nil, action: .openWebView(url: url, simple: false)) case let .keyboardButtonSimpleWebView(text, url): self.init(title: text, titleWhenForwarded: nil, action: .openWebView(url: url, simple: true)) - case let .keyboardButtonRequestPeer(text, buttonId, peerType): + case let .keyboardButtonRequestPeer(text, buttonId, peerType, maxQuantity): let mappedPeerType: ReplyMarkupButtonRequestPeerType switch peerType { case let .requestPeerTypeUser(_, bot, premium): @@ -88,7 +88,7 @@ extension ReplyMarkupButton { botAdminRights: botAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:)) )) } - self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId)) + self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId, maxQuantity: maxQuantity)) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 4a060a74387..60e9e513044 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -242,9 +242,9 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { for id in userIds { result.append(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))) } - case let .messageActionRequestedPeer(_, peer): - result.append(peer.peerId) - case let .messageActionGiftCode(_, boostPeer, _, _): + case let .messageActionRequestedPeer(_, peers): + result.append(contentsOf: peers.map(\.peerId)) + case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _): if let boostPeer = boostPeer { result.append(boostPeer.peerId) } @@ -422,12 +422,21 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI case let .messageMediaStory(flags, peerId, id, _): let isMention = (flags & (1 << 1)) != 0 return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil) - case let .messageMediaGiveaway(apiFlags, channels, countries, quantity, months, untilDate): + case let .messageMediaGiveaway(apiFlags, channels, countries, prizeDescription, quantity, months, untilDate): var flags: TelegramMediaGiveaway.Flags = [] if (apiFlags & (1 << 0)) != 0 { flags.insert(.onlyNewSubscribers) } - return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil, nil) + return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) + case let .messageMediaGiveawayResults(apiFlags, channelId, additionalPeersCount, launchMsgId, winnersCount, unclaimedCount, winners, months, prizeDescription, untilDate): + var flags: TelegramMediaGiveawayResults.Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.onlyNewSubscribers) + } + if (apiFlags & (1 << 2)) != 0 { + flags.insert(.refunded) + } + return (TelegramMediaGiveawayResults(flags: flags, launchMessageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: launchMsgId), additionalChannelsCount: additionalPeersCount ?? 0, winnersPeerIds: winners.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }, winnersCount: winnersCount, unclaimedCount: unclaimedCount, months: months, untilDate: untilDate, prizeDescription: prizeDescription), nil, nil, nil, nil) } } @@ -442,6 +451,8 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { } } switch mediaArea { + case .inputMediaAreaChannelPost: + return nil case .inputMediaAreaVenue: return nil case let .mediaAreaGeoPoint(coordinates, geo): @@ -481,10 +492,12 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { } else { return nil } + case let .mediaAreaChannelPost(coordinates, channelId, messageId): + return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) } } -func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea]) -> [Api.MediaArea] { +func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction) -> [Api.MediaArea] { var apiMediaAreas: [Api.MediaArea] = [] for area in mediaAreas { let coordinates = area.coordinates @@ -507,6 +520,10 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea]) -> [Api.MediaArea] { apiFlags |= (1 << 1) } apiMediaAreas.append(.mediaAreaSuggestedReaction(flags: apiFlags, coordinates: inputCoordinates, reaction: reaction.apiReaction)) + case let .channelMessage(_, messageId): + if let peer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(peer) { + apiMediaAreas.append(.inputMediaAreaChannelPost(coordinates: inputCoordinates, channel: inputChannel, msgId: messageId.id)) + } } } return apiMediaAreas @@ -563,7 +580,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes } extension StoreMessage { - convenience init?(apiMessage: Api.Message, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { + convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId @@ -646,6 +663,7 @@ extension StoreMessage { } var forwardInfo: StoreMessageForwardInfo? + var savedFromPeerId: PeerId? if let fwdFrom = fwdFrom { switch fwdFrom { case let .messageFwdHeader(flags, fromId, fromName, date, channelPost, postAuthor, savedFromPeer, savedFromMsgId, psaType): @@ -675,6 +693,7 @@ extension StoreMessage { if let savedFromPeer = savedFromPeer, let savedFromMsgId = savedFromMsgId { let peerId: PeerId = savedFromPeer.peerId + savedFromPeerId = peerId let messageId: MessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: savedFromMsgId) attributes.append(SourceReferenceMessageAttribute(messageId: messageId)) } @@ -688,6 +707,10 @@ extension StoreMessage { } } } + + if peerId == accountPeerId, let savedFromPeerId = savedFromPeerId { + threadId = savedFromPeerId.toInt64() + } let messageText = message var medias: [Media] = [] diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 20ea820be7a..dda3010fa85 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -49,7 +49,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe case .messageActionScreenshotTaken: return TelegramMediaAction(action: .historyScreenshot) case let .messageActionCustomAction(message): - return TelegramMediaAction(action: .customText(text: message, entities: [])) + return TelegramMediaAction(action: .customText(text: message, entities: [], additionalAttributes: nil)) case let .messageActionBotAllowed(flags, domain, app): if let domain = domain { return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain)) @@ -121,16 +121,16 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .topicEdited(components: components)) case let .messageActionSuggestProfilePhoto(photo): return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo))) - case let .messageActionRequestedPeer(buttonId, peer): - return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerId: peer.peerId)) + case let .messageActionRequestedPeer(buttonId, peers): + return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerIds: peers.map { $0.peerId })) case let .messageActionSetChatWallPaper(flags, wallpaper): if (flags & (1 << 0)) != 0 { return TelegramMediaAction(action: .setSameChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper))) } else { return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0)) } - case let .messageActionGiftCode(flags, boostPeer, months, slug): - return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months)) + case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount): + return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) case .messageActionGiveawayLaunch: return TelegramMediaAction(action: .giveawayLaunched) case let .messageActionGiveawayResults(winners, unclaimed): diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 7ffbcce9016..15899cf081d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -70,6 +70,8 @@ extension StickerPackReference { self = .iconStatusEmoji case .inputStickerSetEmojiDefaultTopicIcons: self = .iconTopicEmoji + case .inputStickerSetEmojiChannelDefaultStatuses: + return nil } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift index 09f80f18824..9bb1a7ae3f6 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/Wallpaper.swift @@ -7,7 +7,7 @@ import TelegramApi extension WallpaperSettings { init(apiWallpaperSettings: Api.WallPaperSettings) { switch apiWallpaperSettings { - case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation): + case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation, emoticon): var colors: [UInt32] = [] if let backgroundColor = backgroundColor { colors.append(UInt32(bitPattern: backgroundColor)) @@ -21,7 +21,7 @@ extension WallpaperSettings { if let fourthBackgroundColor = fourthBackgroundColor { colors.append(UInt32(bitPattern: fourthBackgroundColor)) } - self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation) + self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation, emoticon: emoticon) } } } @@ -42,6 +42,9 @@ func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPap if let _ = wallpaperSettings.intensity { flags |= (1 << 3) } + if let _ = wallpaperSettings.emoticon { + flags |= (1 << 7) + } var secondBackgroundColor: Int32? if wallpaperSettings.colors.count >= 2 { flags |= (1 << 4) @@ -57,7 +60,7 @@ func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPap flags |= (1 << 6) fourthBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[3]) } - return .wallPaperSettings(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0) + return .wallPaperSettings(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0, emoticon: wallpaperSettings.emoticon) } extension TelegramWallpaper { @@ -77,7 +80,11 @@ extension TelegramWallpaper { self = .color(0xffffff) } case let .wallPaperNoFile(id, _, settings): - if let settings = settings, case let .wallPaperSettings(_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation) = settings { + if let settings = settings, case let .wallPaperSettings(_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation, emoticon) = settings { + if id == 0, let emoticon = emoticon { + self = .emoticon(emoticon) + return + } let colors: [UInt32] = ([backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor] as [Int32?]).compactMap({ color -> UInt32? in return color.flatMap(UInt32.init(bitPattern:)) }) @@ -105,6 +112,8 @@ extension TelegramWallpaper { return (.inputWallPaperNoFile(id: 0), apiWallpaperSettings(WallpaperSettings(colors: [color]))) case let .gradient(gradient): return (.inputWallPaperNoFile(id: gradient.id ?? 0), apiWallpaperSettings(WallpaperSettings(colors: gradient.colors, rotation: gradient.settings.rotation))) + case let .emoticon(emoticon): + return (.inputWallPaperNoFile(id: 0), apiWallpaperSettings(WallpaperSettings(emoticon: emoticon))) default: return nil } diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 560eda65ed5..db874759a91 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -224,7 +224,7 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title: for update in result.allUpdates { switch update { case let .updateNewChannelMessage(message, _, _): - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { if case let .Id(id) = message.id { topicId = Int64(id.id) } @@ -570,7 +570,7 @@ enum LoadMessageHistoryThreadsError { case generic } -func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { +func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId, query: String?, offsetIndex: StoredPeerThreadCombinedState.Index?, limit: Int) -> Signal { let signal: Signal = postbox.transaction { transaction -> Api.InputChannel? in guard let channel = transaction.getPeer(peerId) as? TelegramChannel else { return nil @@ -585,7 +585,12 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post guard let inputChannel = inputChannel else { return .fail(.generic) } - let flags: Int32 = 0 + var flags: Int32 = 0 + + if query != nil { + flags |= 1 << 0 + } + var offsetDate: Int32 = 0 var offsetId: Int32 = 0 var offsetTopic: Int32 = 0 @@ -597,7 +602,7 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post let signal: Signal = network.request(Api.functions.channels.getForumTopics( flags: flags, channel: inputChannel, - q: nil, + q: query, offsetDate: offsetDate, offsetId: offsetId, offsetTopic: offsetTopic, @@ -613,7 +618,7 @@ func _internal_requestMessageHistoryThreads(accountPeerId: PeerId, postbox: Post var pinnedIds: [Int64] = [] let addedMessages = messages.compactMap { message -> StoreMessage? in - return StoreMessage(apiMessage: message, peerIsForum: true) + return StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: true) } let _ = pts @@ -839,6 +844,59 @@ func _internal_forumChannelTopicNotificationExceptions(account: Account, id: Eng } } +public func _internal_searchForumTopics(account: Account, peerId: EnginePeer.Id, query: String) -> Signal<[EngineChatList.Item], NoError> { + return _internal_requestMessageHistoryThreads(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, peerId: peerId, query: query, offsetIndex: nil, limit: 100) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[EngineChatList.Item], NoError> in + guard let result else { + return .single([]) + } + return account.postbox.transaction { transcation -> [EngineChatList.Item] in + guard let peer = transcation.getPeer(peerId) else { + return [] + } + + var items: [EngineChatList.Item] = [] + for item in result.items { + guard let index = item.index else { + continue + } + items.append(EngineChatList.Item( + id: .forum(item.threadId), + index: .forum(pinnedIndex: .none, timestamp: index.timestamp, threadId: index.threadId, namespace: Namespaces.Message.Cloud, id: index.messageId), + messages: [], + readCounters: nil, + isMuted: false, + draft: nil, + threadData: item.data, + renderedPeer: EngineRenderedPeer(peer: EnginePeer(peer)), + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + forumTopicData: EngineChatList.ForumTopicData( + id: item.threadId, + title: item.data.info.title, + iconFileId: item.data.info.icon, + iconColor: item.data.info.iconColor, + maxOutgoingReadMessageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: item.data.maxOutgoingReadId), + isUnread: false + ), + topForumTopicItems: [], + hasFailed: false, + isContact: false, + autoremoveTimeout: nil, + storyStats: nil + )) + } + + return items + } + } +} + public final class ForumChannelTopics { private final class Impl { private let queue: Queue @@ -859,8 +917,6 @@ public final class ForumChannelTopics { self.account = account self.peerId = peerId - //let _ = _internal_loadMessageHistoryThreads(account: self.account, peerId: peerId, offsetIndex: nil, limit: 100).start() - self.updateDisposable.set(account.viewTracker.polledChannel(peerId: peerId).start()) } diff --git a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift index 774b18901ca..afa55aabb56 100644 --- a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift @@ -30,7 +30,7 @@ public func requestUpdatesXml(account: Account, source: String) -> Signal mapToSignal { result in switch result { case let .channelMessages(_, _, _, _, apiMessages, _, apiChats, apiUsers): - if let apiMessage = apiMessages.first, let storeMessage = StoreMessage(apiMessage: apiMessage, peerIsForum: peer.isForum) { + if let apiMessage = apiMessages.first, let storeMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: peer.isForum) { var peers: [PeerId: Peer] = [:] for chat in apiChats { @@ -113,7 +113,7 @@ public func downloadAppUpdate(account: Account, source: String, messageId: Int32 } let messageAndFile:(Message, TelegramMediaFile)? = apiMessages.compactMap { value in - return StoreMessage(apiMessage: value, peerIsForum: peer.isForum) + return StoreMessage(apiMessage: value, accountPeerId: account.peerId, peerIsForum: peer.isForum) }.compactMap { value in return locallyRenderedMessage(message: value, peers: peers) }.sorted(by: { diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 84674c29b88..1fb83a9ee5c 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -622,9 +622,9 @@ final class MediaReferenceRevalidationContext { } } - func peerAvatars(postbox: Postbox, network: Network, background: Bool, peer: PeerReference) -> Signal<[TelegramPeerPhoto], RevalidateMediaReferenceError> { + func peerAvatars(accountPeerId: PeerId, postbox: Postbox, network: Network, background: Bool, peer: PeerReference) -> Signal<[TelegramPeerPhoto], RevalidateMediaReferenceError> { return self.genericItem(key: .peerAvatars(peer: peer), background: background, request: { next, error in - return (_internal_requestPeerPhotos(postbox: postbox, network: network, peerId: peer.id) + return (_internal_requestPeerPhotos(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: peer.id) |> mapError { _ -> RevalidateMediaReferenceError in }).start(next: { value in next(value) @@ -812,7 +812,7 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n return .fail(.generic) } case let .avatarList(peer, media): - return revalidationContext.peerAvatars(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) + return revalidationContext.peerAvatars(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) |> mapToSignal { result -> Signal in for photo in result { if let updatedResource = findUpdatedMediaResource(media: photo.image, previousMedia: media, resource: resource) { @@ -911,7 +911,7 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n return .fail(.generic) } case let .avatarList(peer, _): - return revalidationContext.peerAvatars(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) + return revalidationContext.peerAvatars(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) |> mapToSignal { updatedPeerAvatars -> Signal in for photo in updatedPeerAvatars { if let updatedResource = findUpdatedMediaResource(media: photo.image, previousMedia: nil, resource: resource) { diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 258494f0f3f..2914559d88c 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -514,7 +514,7 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa } if useNetworkFramework { - if #available(iOS 12.0, macOS 10.14, *) { + if #available(iOS 12.0, macOS 14.0, *) { context.makeTcpConnectionInterface = { delegate, delegateQueue in return NetworkFrameworkTcpConnectionInterface(delegate: delegate, delegateQueue: delegateQueue) } diff --git a/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift b/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift index 956163880d3..d17d399bb9a 100644 --- a/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift +++ b/submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift @@ -4,7 +4,7 @@ import Network import MtProtoKit import SwiftSignalKit -@available(iOS 12.0, macOS 10.14, *) +@available(iOS 12.0, macOS 14.0, *) final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInterface { private struct ReadRequest { let length: Int diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index bb4bf0d33dc..2532a673a46 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -201,7 +201,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, if let result = result { return postbox.transaction { transaction -> RequestEditMessageResult in var toMedia: Media? - if let message = result.messages.first.flatMap({ StoreMessage(apiMessage: $0, peerIsForum: peer.isForum) }) { + if let message = result.messages.first.flatMap({ StoreMessage(apiMessage: $0, accountPeerId: accountPeerId, peerIsForum: peer.isForum) }) { toMedia = message.media.first } @@ -217,7 +217,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, let peers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers) - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), case let .Id(id) = message.id { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), case let .Id(id) = message.id { transaction.updateMessage(id, update: { previousMessage in var updatedFlags = message.flags var updatedLocalTags = message.localTags diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 8ca7cd73bca..9853581311e 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -736,7 +736,7 @@ func finalStateWithDifference(accountPeerId: PeerId, postbox: Postbox, network: if let peerId = message.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { updatedState.addMessages([message], location: .UpperHistoryBlock) } } @@ -948,7 +948,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let peerId = apiMessage.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum), case let .Id(messageId) = message.id { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum), case let .Id(messageId) = message.id { let peerId = messageId.peerId if let previousState = updatedState.channelStates[peerId] { if previousState.pts >= pts { @@ -1026,7 +1026,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let peerId = apiMessage.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum), case let .Id(messageId) = message.id { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum), case let .Id(messageId) = message.id { if let preCachedResources = apiMessage.preCachedResources { for (resource, data) in preCachedResources { updatedState.addPreCachedResource(resource, data: data) @@ -1051,7 +1051,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let peerId = apiMessage.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { if let previousState = updatedState.channelStates[message.id.peerId] { if previousState.pts >= pts { let messageText: String @@ -1098,7 +1098,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let peerId = apiMessage.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { if let preCachedResources = apiMessage.preCachedResources { for (resource, data) in preCachedResources { updatedState.addPreCachedResource(resource, data: data) @@ -1634,7 +1634,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let peerId = apiMessage.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum, namespace: Namespaces.Message.ScheduledCloud) { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.ScheduledCloud) { updatedState.addScheduledMessages([message]) } case let .updateDeleteScheduledMessages(peer, messages): @@ -1806,13 +1806,13 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } - return resolveForumThreads(postbox: postbox, network: network, state: finalState) + return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) |> mapToSignal { finalState in - return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) + return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: resultingState) |> mapToSignal { resultingState -> Signal in - return resolveMissingPeerChatInfos(network: network, state: resultingState) + return resolveMissingPeerChatInfos(accountPeerId: accountPeerId, network: network, state: resultingState) |> map { resultingState, resolveError -> AccountFinalState in return AccountFinalState(state: resultingState, shouldPoll: shouldPoll || hadError || resolveError, incomplete: missingUpdates, missingUpdatesFromChannels: Set(), discard: resolveError) } @@ -1822,7 +1822,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: } } -func resolveForumThreads(postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { +func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { var forumThreadIds = Set() for operation in state.operations { @@ -1888,7 +1888,7 @@ func resolveForumThreads(postbox: Postbox, network: Network, state: AccountMutab state.mergeUsers(users) for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { storeMessages.append(message) } } @@ -1994,7 +1994,7 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, network: Netwo users.append(contentsOf: apiUsers) for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { storeMessages.append(message) } } @@ -2047,7 +2047,7 @@ func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, network: Netwo } } -func resolveForumThreads(postbox: Postbox, network: Network, fetchedChatList: FetchedChatList) -> Signal { +func resolveForumThreads(accountPeerId: PeerId, postbox: Postbox, network: Network, fetchedChatList: FetchedChatList) -> Signal { var forumThreadIds = Set() for message in fetchedChatList.storeMessages { @@ -2103,7 +2103,7 @@ func resolveForumThreads(postbox: Postbox, network: Network, fetchedChatList: Fe fetchedChatList.peers = fetchedChatList.peers.union(with: AccumulatedPeers(chats: chats, users: users)) for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { fetchedChatList.storeMessages.append(message) } } @@ -2308,14 +2308,14 @@ private func reactionsFromState(_ state: AccountMutableState) -> [MessageReactio return result } -private func resolveAssociatedMessages(postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { +private func resolveAssociatedMessages(accountPeerId: PeerId, postbox: Postbox, network: Network, state: AccountMutableState) -> Signal { let missingReplyMessageIds = state.referencedReplyMessageIds.subtractingStoredIds(state.storedMessages) let missingGeneralMessageIds = state.referencedGeneralMessageIds.subtracting(state.storedMessages) if missingReplyMessageIds.isEmpty && missingGeneralMessageIds.isEmpty { return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: state), reactions: reactionsFromState(state), result: state) |> mapToSignal { state in - return resolveForumThreads(postbox: postbox, network: network, state: state) + return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, state: state) } } else { var missingPeers = false @@ -2405,7 +2405,7 @@ private func resolveAssociatedMessages(postbox: Postbox, network: Network, state if let peerId = message.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { storeMessages.append(message) } } @@ -2417,13 +2417,13 @@ private func resolveAssociatedMessages(postbox: Postbox, network: Network, state |> mapToSignal { updatedState -> Signal in return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: messagesFromOperations(state: updatedState), reactions: reactionsFromState(updatedState), result: updatedState) |> mapToSignal { state in - return resolveForumThreads(postbox: postbox, network: network, state: state) + return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, state: state) } } } } -private func resolveMissingPeerChatInfos(network: Network, state: AccountMutableState) -> Signal<(AccountMutableState, Bool), NoError> { +private func resolveMissingPeerChatInfos(accountPeerId: PeerId, network: Network, state: AccountMutableState) -> Signal<(AccountMutableState, Bool), NoError> { var missingPeers: [PeerId: Api.InputPeer] = [:] var hadError = false @@ -2531,7 +2531,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable if let peerId = message.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var updatedStoreMessage = storeMessage if case let .Id(id) = storeMessage.id { if let channelState = channelStates[id.peerId] { @@ -2588,12 +2588,12 @@ func pollChannelOnce(accountPeerId: PeerId, postbox: Postbox, network: Network, let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in - return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) + return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: finalState) } |> mapToSignal { resultingState -> Signal in - return resolveMissingPeerChatInfos(network: network, state: resultingState) + return resolveMissingPeerChatInfos(accountPeerId: accountPeerId, network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false, missingUpdatesFromChannels: Set(), discard: false) } @@ -2645,12 +2645,12 @@ public func standalonePollChannelOnce(accountPeerId: PeerId, postbox: Postbox, n let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in - return resolveAssociatedMessages(postbox: postbox, network: network, state: finalState) + return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) |> mapToSignal { resultingState -> Signal in return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: finalState) } |> mapToSignal { resultingState -> Signal in - return resolveMissingPeerChatInfos(network: network, state: resultingState) + return resolveMissingPeerChatInfos(accountPeerId: accountPeerId, network: network, state: resultingState) |> map { resultingState, _ -> AccountFinalState in return AccountFinalState(state: resultingState, shouldPoll: false, incomplete: false, missingUpdatesFromChannels: Set(), discard: false) } @@ -2770,7 +2770,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe if let peerId = message.peerId { peerIsForum = updatedState.isPeerForum(peerId: peerId) } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var updatedStoreMessage = storeMessage if case let .Id(id) = storeMessage.id { if let channelState = channelStates[id.peerId] { @@ -2833,7 +2833,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe var resetTopicsSignals: [Signal] = [] for resetForumTopicPeerId in resetForumTopics { - resetTopicsSignals.append(_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: resetForumTopicPeerId, offsetIndex: nil, limit: 20) + resetTopicsSignals.append(_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: resetForumTopicPeerId, query: nil, offsetIndex: nil, limit: 20) |> map(StateResetForumTopics.result) |> `catch` { _ -> Signal in return .single(.error(resetForumTopicPeerId)) @@ -2855,7 +2855,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe } // TODO: delete messages later than top - return resolveAssociatedMessages(postbox: postbox, network: network, state: updatedState) + return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: updatedState) |> mapToSignal { resultingState -> Signal in return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: updatedState) } @@ -2918,7 +2918,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo if let peerId = apiMessage.peerId, updatedState.isPeerForum(peerId: peerId) { peerIsForum = true } - if var message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum) { + if var message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var attributes = message.attributes attributes.append(ChannelMessageStateVersionAttribute(pts: pts)) message = message.withUpdatedAttributes(attributes) @@ -2956,7 +2956,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo if let peerId = apiMessage.peerId, updatedState.isPeerForum(peerId: peerId) { peerIsForum = true } - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum), case let .Id(messageId) = message.id, messageId.peerId == peer.id { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum), case let .Id(messageId) = message.id, messageId.peerId == peer.id { if let preCachedResources = apiMessage.preCachedResources { for (resource, data) in preCachedResources { updatedState.addPreCachedResource(resource, data: data) @@ -3017,7 +3017,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo } } - return resolveForumThreads(postbox: postbox, network: network, state: updatedState) + return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, state: updatedState) |> mapToSignal { updatedState in return resolveAssociatedStories(postbox: postbox, network: network, accountPeerId: accountPeerId, state: updatedState) |> map { updatedState -> (AccountMutableState, Bool, Int32?) in @@ -3076,7 +3076,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo resetForumTopics.insert(peer.peerId) for apiMessage in messages { - if var message = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum) { + if var message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var attributes = message.attributes attributes.append(ChannelMessageStateVersionAttribute(pts: pts)) message = message.withUpdatedAttributes(attributes) @@ -3113,7 +3113,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo var resetTopicsSignals: [Signal] = [] for resetForumTopicPeerId in resetForumTopics { - resetTopicsSignals.append(_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: resetForumTopicPeerId, offsetIndex: nil, limit: 20) + resetTopicsSignals.append(_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: resetForumTopicPeerId, query: nil, offsetIndex: nil, limit: 20) |> map(StateResetForumTopics.result) |> `catch` { _ -> Signal in return .single(.error(resetForumTopicPeerId)) @@ -4638,6 +4638,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -4670,6 +4671,7 @@ func replayFinalState( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1601b20c240..3f7469dabe1 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -55,17 +55,20 @@ public final class AccountStateManager { public let callAccessHash: Int64 public let timestamp: Int32 public let peer: EnginePeer + public let isVideo: Bool init( callId: Int64, callAccessHash: Int64, timestamp: Int32, - peer: EnginePeer + peer: EnginePeer, + isVideo: Bool ) { self.callId = callId self.callAccessHash = callAccessHash self.timestamp = timestamp self.peer = peer + self.isVideo = isVideo } } @@ -1821,7 +1824,7 @@ public final class AccountStateManager { switch update { case let .updatePhoneCall(phoneCall): switch phoneCall { - case let .phoneCallRequested(_, id, accessHash, date, adminId, _, _, _): + case let .phoneCallRequested(flags, id, accessHash, date, adminId, _, _, _): guard let peer = peers.first(where: { $0.id == PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)) }) else { return nil } @@ -1829,7 +1832,8 @@ public final class AccountStateManager { callId: id, callAccessHash: accessHash, timestamp: date, - peer: EnginePeer(peer) + peer: EnginePeer(peer), + isVideo: (flags & (1 << 6)) != 0 ) default: break diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index f18a722046a..2887726a174 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -118,7 +118,6 @@ final class AccountTaskManager { tasks.add(managedAutodownloadSettingsUpdates(accountManager: self.accountManager, network: self.stateManager.network).start()) tasks.add(managedTermsOfServiceUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedAppUpdateInfo(network: self.stateManager.network, stateManager: self.stateManager).start()) - tasks.add(managedAppChangelog(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, appVersion: self.networkArguments.appVersion).start()) tasks.add(managedPromoInfoUpdates(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, viewTracker: self.viewTracker).start()) tasks.add(managedLocalizationUpdatesOperations(accountManager: self.accountManager, postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedPendingPeerNotificationSettings(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index d2d19e91df2..db150f34fb4 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -109,7 +109,7 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal Signal { - return stateManager.isUpdating - |> filter { !$0 } - |> take(1) - |> mapToSignal { _ -> Signal in - return postbox.transaction { transaction -> AppChangelogState in - return transaction.getPreferencesEntry(key: PreferencesKeys.appChangelogState)?.get(AppChangelogState.self) ?? AppChangelogState.default - } - |> mapToSignal { appChangelogState -> Signal in - let appChangelogState = appChangelogState - if appChangelogState.checkedVersion == appVersion { - return .complete() - } - let previousVersion = appChangelogState.previousVersion - return network.request(Api.functions.help.getAppChangelog(prevAppVersion: previousVersion)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { updates -> Signal in - if let updates = updates { - stateManager.addUpdates(updates) - } - - return postbox.transaction { transaction in - updateAppChangelogState(transaction: transaction, { state in - var state = state - state.checkedVersion = appVersion - state.previousVersion = appVersion - return state - }) - } - } - } - } + return .never() } diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 8407eaf2320..5f6ac5c48ef 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -151,7 +151,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var attributes: [MessageAttribute] let text: String let forwardInfo: StoreMessageForwardInfo? - if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { + if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { media = updatedMessage.media attributes = updatedMessage.attributes text = updatedMessage.text @@ -358,7 +358,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage } } - if let resultMessage = StoreMessage(apiMessage: apiMessage, peerIsForum: peerIsForum, namespace: namespace), case let .Id(id) = resultMessage.id { + if let resultMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: stateManager.accountPeerId, peerIsForum: peerIsForum, namespace: namespace), case let .Id(id) = resultMessage.id { resultMessages[id] = resultMessage } } diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index 51bb27ee041..9eb0b60a9e3 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -656,12 +656,24 @@ private final class CallSessionManagerContext { if let (id, accessHash, reason) = dropData { self.contextIdByStableId.removeValue(forKey: id) - let mappedReason: CallSessionTerminationReason = .ended(.hungUp) + let mappedReason: CallSessionTerminationReason + switch reason { + case .abort: + mappedReason = .ended(.hungUp) + case .busy: + mappedReason = .ended(.busy) + case .disconnect: + mappedReason = .error(.disconnected) + case .hangUp: + mappedReason = .ended(.hungUp) + case .missed: + mappedReason = .ended(.missed) + } context.state = .dropping(reason: mappedReason, disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: reason) |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in if let strongSelf = self { if let context = strongSelf.contexts[internalId] { - context.state = .terminated(id: id, accessHash: accessHash, reason: .ended(.hungUp), reportRating: reportRating, sendDebugLogs: sendDebugLogs) + context.state = .terminated(id: id, accessHash: accessHash, reason: mappedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) /*if sendDebugLogs { let network = strongSelf.network let _ = (debugLog diff --git a/submodules/TelegramCore/Sources/State/FetchChatList.swift b/submodules/TelegramCore/Sources/State/FetchChatList.swift index 22fbe954a35..76b0d110eee 100644 --- a/submodules/TelegramCore/Sources/State/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/State/FetchChatList.swift @@ -47,7 +47,7 @@ private func extractDialogsData(peerDialogs: Api.messages.PeerDialogs) -> (apiDi } } -private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], apiChats: [Api.Chat], apiUsers: [Api.User], apiIsAtLowestBoundary: Bool) -> ParsedDialogs { +private func parseDialogs(accountPeerId: PeerId, apiDialogs: [Api.Dialog], apiMessages: [Api.Message], apiChats: [Api.Chat], apiUsers: [Api.User], apiIsAtLowestBoundary: Bool) -> ParsedDialogs { var notificationSettings: [PeerId: PeerNotificationSettings] = [:] var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:] var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:] @@ -148,7 +148,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], if let peerId = message.peerId, let peer = peers.get(peerId), peer.isForum { peerIsForum = true } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var updatedStoreMessage = storeMessage if case let .Id(id) = storeMessage.id { if let channelPts = channelStates[id.peerId] { @@ -207,7 +207,7 @@ struct FetchedChatList { var threadInfos: [MessageId: StoreMessageHistoryThreadData] } -func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLocation, upperBound: MessageIndex, hash: Int64, limit: Int32) -> Signal { +func fetchChatList(accountPeerId: PeerId, postbox: Postbox, network: Network, location: FetchChatListLocation, upperBound: MessageIndex, hash: Int64, limit: Int32) -> Signal { return postbox.stateView() |> mapToSignal { view -> Signal in if let state = view.state as? AuthorizedAccountState { @@ -266,11 +266,11 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo return .single(nil) } let extractedRemoteDialogs = extractDialogsData(dialogs: remoteChats) - let parsedRemoteChats = parseDialogs(apiDialogs: extractedRemoteDialogs.apiDialogs, apiMessages: extractedRemoteDialogs.apiMessages, apiChats: extractedRemoteDialogs.apiChats, apiUsers: extractedRemoteDialogs.apiUsers, apiIsAtLowestBoundary: extractedRemoteDialogs.apiIsAtLowestBoundary) + let parsedRemoteChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedRemoteDialogs.apiDialogs, apiMessages: extractedRemoteDialogs.apiMessages, apiChats: extractedRemoteDialogs.apiChats, apiUsers: extractedRemoteDialogs.apiUsers, apiIsAtLowestBoundary: extractedRemoteDialogs.apiIsAtLowestBoundary) var parsedPinnedChats: ParsedDialogs? if let pinnedChats = pinnedChats { let extractedPinnedChats = extractDialogsData(peerDialogs: pinnedChats) - parsedPinnedChats = parseDialogs(apiDialogs: extractedPinnedChats.apiDialogs, apiMessages: extractedPinnedChats.apiMessages, apiChats: extractedPinnedChats.apiChats, apiUsers: extractedPinnedChats.apiUsers, apiIsAtLowestBoundary: extractedPinnedChats.apiIsAtLowestBoundary) + parsedPinnedChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedPinnedChats.apiDialogs, apiMessages: extractedPinnedChats.apiMessages, apiChats: extractedPinnedChats.apiChats, apiUsers: extractedPinnedChats.apiUsers, apiIsAtLowestBoundary: extractedPinnedChats.apiIsAtLowestBoundary) } var combinedReferencedFolders = Set() @@ -287,7 +287,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo |> retryRequest |> map { result -> (PeerGroupId, ParsedDialogs) in let extractedData = extractDialogsData(dialogs: result) - let parsedChats = parseDialogs(apiDialogs: extractedData.apiDialogs, apiMessages: extractedData.apiMessages, apiChats: extractedData.apiChats, apiUsers: extractedData.apiUsers, apiIsAtLowestBoundary: extractedData.apiIsAtLowestBoundary) + let parsedChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedData.apiDialogs, apiMessages: extractedData.apiMessages, apiChats: extractedData.apiChats, apiUsers: extractedData.apiUsers, apiIsAtLowestBoundary: extractedData.apiIsAtLowestBoundary) return (groupId, parsedChats) } folderSignals.append(requestFeed) @@ -399,7 +399,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, reactions: [], result: result) |> mapToSignal { result in if let result = result { - return resolveForumThreads(postbox: postbox, network: network, fetchedChatList: result) + return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, fetchedChatList: result) |> map(Optional.init) } else { return .single(result) diff --git a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift index cc389c3faa7..266e2c68034 100644 --- a/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift +++ b/submodules/TelegramCore/Sources/State/HistoryViewStateValidation.swift @@ -687,7 +687,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran var storeMessages: [StoreMessage] = [] for message in messages { - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: topPeer.isForum, namespace: messageNamespace) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: topPeer.isForum, namespace: messageNamespace) { var attributes = storeMessage.attributes if let channelPts = channelPts { attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) @@ -734,7 +734,7 @@ private func validateBatch(postbox: Postbox, network: Network, transaction: Tran } var ids = Set() for message in apiMessages { - if let parsedMessage = StoreMessage(apiMessage: message, peerIsForum: topPeer.isForum, namespace: messageNamespace), case let .Id(id) = parsedMessage.id { + if let parsedMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: topPeer.isForum, namespace: messageNamespace), case let .Id(id) = parsedMessage.id { if let tag = tag { if parsedMessage.tags.contains(tag) { ids.insert(id) @@ -938,7 +938,7 @@ private func validateReplyThreadBatch(postbox: Postbox, network: Network, transa var storeMessages: [StoreMessage] = [] for message in messages { - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: topPeer.isForum, namespace: messageNamespace) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: topPeer.isForum, namespace: messageNamespace) { var attributes = storeMessage.attributes if let channelPts = channelPts { attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) @@ -983,7 +983,7 @@ private func validateReplyThreadBatch(postbox: Postbox, network: Network, transa } var ids = Set() for message in apiMessages { - if let parsedMessage = StoreMessage(apiMessage: message, peerIsForum: topPeer.isForum, namespace: messageNamespace), case let .Id(id) = parsedMessage.id { + if let parsedMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: topPeer.isForum, namespace: messageNamespace), case let .Id(id) = parsedMessage.id { ids.insert(id) } } diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index cbd0814b5a2..d8537f8d103 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -297,7 +297,7 @@ func withResolvedAssociatedMessages(postbox: Postbox, source: FetchMessageHis for (peer, messages, chats, users) in results { if !messages.isEmpty { for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum) { additionalMessages.append(message) } } @@ -732,7 +732,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH var storeMessages: [StoreMessage] = [] for message in messages { - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peer.isForum, namespace: namespace) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum, namespace: namespace) { if let channelPts = channelPts { var attributes = storeMessage.attributes attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts)) @@ -870,7 +870,7 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId case .group: location = .group(groupId) } - return fetchChatList(postbox: postbox, network: network, location: location, upperBound: hole.index, hash: 0, limit: 100) + return fetchChatList(accountPeerId: accountPeerId, postbox: postbox, network: network, location: location, upperBound: hole.index, hash: 0, limit: 100) |> mapToSignal { fetchedChats -> Signal in guard let fetchedChats = fetchedChats else { return postbox.transaction { transaction -> Void in @@ -1009,7 +1009,7 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForum { peerIsForum = true } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { storeMessages.append(storeMessage) if let index = storeMessage.index, topIndex == nil || index < topIndex! { topIndex = index diff --git a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift index 8f115d7816f..84ade74719e 100644 --- a/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/State/ManagedChatListHoles.swift @@ -143,7 +143,7 @@ func managedForumTopicListHoles(network: Network, postbox: Postbox, accountPeerI } for (entry, disposable) in added { - disposable.set((_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, offsetIndex: entry.index, limit: 100) + disposable.set((_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, query: nil, offsetIndex: entry.index, limit: 100) |> mapToSignal { result -> Signal in return postbox.transaction { transaction in return applyLoadMessageHistoryThreadsResults(accountPeerId: accountPeerId, transaction: transaction, results: [result]) diff --git a/submodules/TelegramCore/Sources/State/ManagedPeerColorUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedPeerColorUpdates.swift index 3b714143c3d..76c5dc6fcce 100644 --- a/submodules/TelegramCore/Sources/State/ManagedPeerColorUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedPeerColorUpdates.swift @@ -94,16 +94,19 @@ public final class EngineAvailableColorOptions: Codable, Equatable { case light = "l" case dark = "d" case isHidden = "h" + case requiredChannelMinBoostLevel = "rcmb" } public let light: ColorOption public let dark: ColorOption? public let isHidden: Bool + public let requiredChannelMinBoostLevel: Int32? - public init(light: ColorOption, dark: ColorOption?, isHidden: Bool) { + public init(light: ColorOption, dark: ColorOption?, isHidden: Bool, requiredChannelMinBoostLevel: Int32?) { self.light = light self.dark = dark self.isHidden = isHidden + self.requiredChannelMinBoostLevel = requiredChannelMinBoostLevel } public init(from decoder: Decoder) throws { @@ -112,6 +115,7 @@ public final class EngineAvailableColorOptions: Codable, Equatable { self.light = try container.decode(ColorOption.self, forKey: .light) self.dark = try container.decodeIfPresent(ColorOption.self, forKey: .dark) self.isHidden = try container.decode(Bool.self, forKey: .isHidden) + self.requiredChannelMinBoostLevel = try container.decodeIfPresent(Int32.self, forKey: .requiredChannelMinBoostLevel) } public func encode(to encoder: Encoder) throws { @@ -120,6 +124,7 @@ public final class EngineAvailableColorOptions: Codable, Equatable { try container.encode(self.light, forKey: .light) try container.encodeIfPresent(self.dark, forKey: .dark) try container.encodeIfPresent(self.isHidden, forKey: .isHidden) + try container.encodeIfPresent(self.requiredChannelMinBoostLevel, forKey: .requiredChannelMinBoostLevel) } public static func ==(lhs: ColorOptionPack, rhs: ColorOptionPack) -> Bool { @@ -135,6 +140,9 @@ public final class EngineAvailableColorOptions: Codable, Equatable { if lhs.isHidden != rhs.isHidden { return false } + if lhs.requiredChannelMinBoostLevel != rhs.requiredChannelMinBoostLevel { + return false + } return true } } @@ -262,14 +270,14 @@ private extension EngineAvailableColorOptions { var mappedOptions: [Option] = [] for apiColor in apiColors { switch apiColor { - case let .peerColorOption(flags, colorId, colors, darkColors): + case let .peerColorOption(flags, colorId, colors, darkColors, requiredChannelMinBoostLevel): let isHidden = (flags & (1 << 0)) != 0 let mappedColors = colors.flatMap(EngineAvailableColorOptions.ColorOption.init(apiColors:)) let mappedDarkColors = darkColors.flatMap(EngineAvailableColorOptions.ColorOption.init(apiColors:)) if let mappedColors = mappedColors { - mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: mappedColors, dark: mappedDarkColors, isHidden: isHidden))) + mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: mappedColors, dark: mappedDarkColors, isHidden: isHidden, requiredChannelMinBoostLevel: requiredChannelMinBoostLevel))) } else if colorId >= 0 && colorId <= 6 { let staticMap: [UInt32] = [ 0xcc5049, @@ -282,7 +290,7 @@ private extension EngineAvailableColorOptions { ] let colorPack = MultiColorPack(colors: [staticMap[Int(colorId)]]) let defaultColors = EngineAvailableColorOptions.ColorOption(palette: colorPack, background: colorPack, stories: nil) - mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: defaultColors, dark: nil, isHidden: isHidden))) + mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: defaultColors, dark: nil, isHidden: isHidden, requiredChannelMinBoostLevel: requiredChannelMinBoostLevel))) } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index daf468b03b0..bcaf3dbe7e6 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -1876,6 +1876,12 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po if let value = contents.media { media.append(value) } + + var attributes = contents.attributes + if !attributes.contains(where: { $0 is AutoremoveTimeoutMessageAttribute }), let messageAutoremoveTimeout = state.messageAutoremoveTimeout { + attributes.append(AutoclearTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil)) + } + let message = Message( stableId: 1, stableVersion: 0, @@ -1892,7 +1898,7 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po forwardInfo: nil, author: nil, text: contents.text, - attributes: contents.attributes, + attributes: attributes, media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), @@ -1967,7 +1973,7 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po } let entitiesAttribute = message.textEntitiesAttribute - let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: contents.attributes, media: updatedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) + let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: updatedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) let storedMessage = StoreMessage( peerId: peerId, diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift index a400c77eda0..4dea62753c5 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift @@ -192,7 +192,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForum { peerIsForum = true } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { storeMessages.append(storeMessage) } } diff --git a/submodules/TelegramCore/Sources/State/ResetState.swift b/submodules/TelegramCore/Sources/State/ResetState.swift index 2e7d2cbab3a..fe9ddcf50ff 100644 --- a/submodules/TelegramCore/Sources/State/ResetState.swift +++ b/submodules/TelegramCore/Sources/State/ResetState.swift @@ -7,7 +7,7 @@ func _internal_resetAccountState(postbox: Postbox, network: Network, accountPeer return network.request(Api.functions.updates.getState()) |> retryRequest |> mapToSignal { state -> Signal in - let chatList = fetchChatList(postbox: postbox, network: network, location: .general, upperBound: .absoluteUpperBound(), hash: 0, limit: 100) + let chatList = fetchChatList(accountPeerId: accountPeerId, postbox: postbox, network: network, location: .general, upperBound: .absoluteUpperBound(), hash: 0, limit: 100) return chatList |> mapToSignal { fetchedChats -> Signal in diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 89018bee15c..1db7e2bbec4 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 167 + return 169 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 863289600d6..534baeba6b7 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -182,7 +182,7 @@ extension Api.Chat { return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) case let .chatForbidden(id, _): return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) - case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) case let .channelForbidden(_, id, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 6735c5c1292..d833c3e2a9a 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -51,7 +51,7 @@ public struct UserLimitsConfiguration: Equatable { maxStoriesSuggestedReactions: 1, maxGiveawayChannelsCount: 10, maxGiveawayCountriesCount: 10, - maxGiveawayPeriodSeconds: 86400 * 7, + maxGiveawayPeriodSeconds: 86400 * 31, maxChannelRecommendationsCount: 10 ) } diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index da541d7f12a..1c0b69c35e5 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -201,8 +201,7 @@ public final class StoryStatsContext { private final class StoryStatsPublicForwardsContextImpl { private let queue: Queue private let account: Account - private let peerId: EnginePeer.Id - private let storyId: Int32 + private let subject: StoryStatsPublicForwardsContext.Subject private let disposable = MetaDisposable() private var isLoadingMore: Bool = false private var hasLoadedOnce: Bool = false @@ -213,11 +212,10 @@ private final class StoryStatsPublicForwardsContextImpl { let state = Promise() - init(queue: Queue, account: Account, peerId: EnginePeer.Id, storyId: Int32) { + init(queue: Queue, account: Account, subject: StoryStatsPublicForwardsContext.Subject) { self.queue = queue self.account = account - self.peerId = peerId - self.storyId = storyId + self.subject = subject self.count = 0 @@ -239,22 +237,41 @@ private final class StoryStatsPublicForwardsContextImpl { self.isLoadingMore = true let account = self.account let accountPeerId = account.peerId - let peerId = self.peerId - let storyId = self.storyId + let subject = self.subject let lastOffset = self.lastOffset - self.disposable.set((self.account.postbox.transaction { transaction -> (Api.InputPeer, Int32?)? in + self.disposable.set((self.account.postbox.transaction { transaction -> (Peer, Int32?)? in + let peerId: PeerId + switch subject { + case let .story(peerIdValue, _): + peerId = peerIdValue + case let .message(messageId): + peerId = messageId.peerId + } let statsDatacenterId = (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.statsDatacenterId - guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else { + guard let peer = transaction.getPeer(peerId) else { return nil } - return (inputPeer, statsDatacenterId) + return (peer, statsDatacenterId) } |> mapToSignal { data -> Signal<([StoryStatsPublicForwardsContext.State.Forward], Int32, String?), NoError> in - if let (inputPeer, statsDatacenterId) = data { + if let (peer, statsDatacenterId) = data { let offset = lastOffset ?? "" - let request = Api.functions.stats.getStoryPublicForwards(peer: inputPeer, id: storyId, offset: offset, limit: 50) + let request: (FunctionDescription, Buffer, DeserializeFunctionResponse) + switch subject { + case let .story(_, id): + guard let inputPeer = apiInputPeer(peer) else { + return .complete() + } + request = Api.functions.stats.getStoryPublicForwards(peer: inputPeer, id: id, offset: offset, limit: 50) + case let .message(messageId): + guard let inputChannel = apiInputChannel(peer) else { + return .complete() + } + request = Api.functions.stats.getMessagePublicForwards(channel: inputChannel, msgId: messageId.id, offset: offset, limit: 50) + } + let signal: Signal if let statsDatacenterId = statsDatacenterId, account.network.datacenterId != statsDatacenterId { signal = account.network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) @@ -294,17 +311,17 @@ private final class StoryStatsPublicForwardsContextImpl { for forward in forwards { switch forward { case let .publicForwardMessage(apiMessage): - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: false), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: false), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { resultForwards.append(.message(EngineMessage(renderedMessage))) } case let .publicForwardStory(apiPeer, apiStory): - if let storedItem = Stories.StoredItem(apiStoryItem: apiStory, peerId: apiPeer.peerId, transaction: transaction), case let .item(item) = storedItem, let media = item.media, let peer = peers[apiPeer.peerId] { let mappedItem = EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -404,10 +421,15 @@ public final class StoryStatsPublicForwardsContext { } } - public init(account: Account, peerId: EnginePeer.Id, storyId: Int32) { + public enum Subject { + case story(peerId: EnginePeer.Id, id: Int32) + case message(messageId: EngineMessage.Id) + } + + public init(account: Account, subject: Subject) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return StoryStatsPublicForwardsContextImpl(queue: queue, account: account, peerId: peerId, storyId: storyId) + return StoryStatsPublicForwardsContextImpl(queue: queue, account: account, subject: subject) }) } diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 792f97e781b..970940e0fdc 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -12,6 +12,7 @@ public enum ServerProvidedSuggestion: String { case upgradePremium = "PREMIUM_UPGRADE" case annualPremium = "PREMIUM_ANNUAL" case restorePremium = "PREMIUM_RESTORE" + case xmasPremiumGift = "PREMIUM_CHRISTMAS" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 4d86257d8ca..c4d1462963b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -259,6 +259,7 @@ public final class CachedChannelData: CachedPeerData { public let allowedReactions: EnginePeerCachedInfoItem public let membersHidden: EnginePeerCachedInfoItem public let viewForumAsMessages: EnginePeerCachedInfoItem + public let wallpaper: TelegramWallpaper? public let peerIds: Set public let messageIds: Set @@ -299,6 +300,7 @@ public final class CachedChannelData: CachedPeerData { self.allowedReactions = .unknown self.membersHidden = .unknown self.viewForumAsMessages = .unknown + self.wallpaper = nil } public init( @@ -331,7 +333,8 @@ public final class CachedChannelData: CachedPeerData { sendAsPeerId: PeerId?, allowedReactions: EnginePeerCachedInfoItem, membersHidden: EnginePeerCachedInfoItem, - viewForumAsMessages: EnginePeerCachedInfoItem + viewForumAsMessages: EnginePeerCachedInfoItem, + wallpaper: TelegramWallpaper? ) { self.isNotAccessible = isNotAccessible self.flags = flags @@ -363,6 +366,7 @@ public final class CachedChannelData: CachedPeerData { self.allowedReactions = allowedReactions self.membersHidden = membersHidden self.viewForumAsMessages = viewForumAsMessages + self.wallpaper = wallpaper var peerIds = Set() for botInfo in botInfos { @@ -390,123 +394,127 @@ public final class CachedChannelData: CachedPeerData { } public func withUpdatedIsNotAccessible(_ isNotAccessible: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedFlags(_ flags: CachedChannelFlags) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedAbout(_ about: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedParticipantsSummary(_ participantsSummary: CachedChannelParticipantsSummary) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedExportedInvitation(_ exportedInvitation: ExportedInvitation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedBotInfos(_ botInfos: [CachedPeerBotInfo]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedPeerStatusSettings(_ peerStatusSettings: PeerStatusSettings?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedStickerPack(_ stickerPack: StickerPackCollectionInfo?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedMinAvailableMessageId(_ minAvailableMessageId: MessageId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedMigrationReference(_ migrationReference: ChannelMigrationReference?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedLinkedDiscussionPeerId(_ linkedDiscussionPeerId: LinkedDiscussionPeerId) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedSlowModeTimeout(_ slowModeTimeout: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedSlowModeValidUntilTimestamp(_ slowModeValidUntilTimestamp: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedStatsDatacenterId(_ statsDatacenterId: Int32) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedInvitedBy(_ invitedBy: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedInvitedOn(_ invitedOn: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedPhoto(_ photo: TelegramMediaImage?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedActiveCall(_ activeCall: ActiveCall?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedAutoremoveTimeout(_ autoremoveTimeout: CachedPeerAutoremoveTimeout) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedPendingSuggestions(_ pendingSuggestions: [String]) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedThemeEmoticon(_ themeEmoticon: String?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedInviteRequestsPending(_ inviteRequestsPending: Int32?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedSendAsPeerId(_ sendAsPeerId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedAllowedReactions(_ allowedReactions: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedMembersHidden(_ membersHidden: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: membersHidden, viewForumAsMessages: self.viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper) } public func withUpdatedViewForumAsMessages(_ viewForumAsMessages: EnginePeerCachedInfoItem) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: viewForumAsMessages) + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: viewForumAsMessages, wallpaper: self.wallpaper) + } + + public func withUpdatedWallpaper(_ wallpaper: TelegramWallpaper?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, themeEmoticon: self.themeEmoticon, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, allowedReactions: self.allowedReactions, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: wallpaper) } public init(decoder: PostboxDecoder) { @@ -616,6 +624,8 @@ public final class CachedChannelData: CachedPeerData { self.viewForumAsMessages = .unknown } + self.wallpaper = decoder.decode(TelegramWallpaperNativeCodable.self, forKey: "wp")?.value + if case let .known(linkedDiscussionPeerIdValue) = self.linkedDiscussionPeerId { if let linkedDiscussionPeerIdValue = linkedDiscussionPeerIdValue { peerIds.insert(linkedDiscussionPeerIdValue) @@ -780,6 +790,12 @@ public final class CachedChannelData: CachedPeerData { case let .known(value): encoder.encode(PeerViewForumAsMessages(value: value), forKey: "viewForumAsMessages") } + + if let wallpaper = self.wallpaper { + encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wp") + } else { + encoder.encodeNil(forKey: "wp") + } } public func isEqual(to: CachedPeerData) -> Bool { @@ -906,6 +922,9 @@ public final class CachedChannelData: CachedPeerData { if other.viewForumAsMessages != self.viewForumAsMessages { return false } + if other.wallpaper != self.wallpaper { + return false + } return true } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift index e07e11baed8..acd70cce706 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift @@ -231,7 +231,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case setupPoll(isQuiz: Bool?) case openUserProfile(peerId: PeerId) case openWebView(url: String, simple: Bool) - case requestPeer(peerType: ReplyMarkupButtonRequestPeerType, buttonId: Int32) + case requestPeer(peerType: ReplyMarkupButtonRequestPeerType, buttonId: Int32, maxQuantity: Int32) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -260,7 +260,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case 11: self = .openWebView(url: decoder.decodeStringForKey("u", orElse: ""), simple: decoder.decodeInt32ForKey("s", orElse: 0) != 0) case 12: - self = .requestPeer(peerType: decoder.decode(ReplyMarkupButtonRequestPeerType.self, forKey: "pt") ?? ReplyMarkupButtonRequestPeerType.user(ReplyMarkupButtonRequestPeerType.User(isBot: nil, isPremium: nil)), buttonId: decoder.decodeInt32ForKey("b", orElse: 0)) + self = .requestPeer(peerType: decoder.decode(ReplyMarkupButtonRequestPeerType.self, forKey: "pt") ?? ReplyMarkupButtonRequestPeerType.user(ReplyMarkupButtonRequestPeerType.User(isBot: nil, isPremium: nil)), buttonId: decoder.decodeInt32ForKey("b", orElse: 0), maxQuantity: decoder.decodeInt32ForKey("q", orElse: 1)) default: self = .text } @@ -308,10 +308,11 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { encoder.encodeInt32(11, forKey: "v") encoder.encodeString(url, forKey: "u") encoder.encodeInt32(simple ? 1 : 0, forKey: "s") - case let .requestPeer(peerType, buttonId): + case let .requestPeer(peerType, buttonId, maxQuantity): encoder.encodeInt32(12, forKey: "v") encoder.encodeInt32(buttonId, forKey: "b") encoder.encode(peerType, forKey: "pt") + encoder.encodeInt32(maxQuantity, forKey: "q") } } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift index f0898df6e31..2c40edf7987 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift @@ -30,6 +30,9 @@ public struct StickerPackCollectionInfoFlags: OptionSet { if flags.contains(StickerPackCollectionInfoFlags.isEmoji) { rawValue |= StickerPackCollectionInfoFlags.isEmoji.rawValue } + if flags.contains(StickerPackCollectionInfoFlags.isAvailableAsChannelStatus) { + rawValue |= StickerPackCollectionInfoFlags.isAvailableAsChannelStatus.rawValue + } self.rawValue = rawValue } @@ -39,6 +42,7 @@ public struct StickerPackCollectionInfoFlags: OptionSet { public static let isAnimated = StickerPackCollectionInfoFlags(rawValue: 1 << 2) public static let isVideo = StickerPackCollectionInfoFlags(rawValue: 1 << 3) public static let isEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 4) + public static let isAvailableAsChannelStatus = StickerPackCollectionInfoFlags(rawValue: 1 << 5) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 08abf2fdabe..717c902f4ef 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -171,6 +171,8 @@ public final class TelegramChannel: Peer, Equatable { public let backgroundEmojiId: Int64? public let profileColor: PeerNameColor? public let profileBackgroundEmojiId: Int64? + public let emojiStatus: PeerEmojiStatus? + public let approximateBoostLevel: Int32? public var indexName: PeerIndexNameRepresentation { var addressNames = self.usernames.map { $0.username } @@ -181,8 +183,19 @@ public final class TelegramChannel: Peer, Equatable { } public var associatedMediaIds: [MediaId]? { - if let backgroundEmojiId = self.backgroundEmojiId { - return [MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId)] + if let emojiStatus = self.emojiStatus, let backgroundEmojiId = self.backgroundEmojiId { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId), + MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) + ] + } else if let emojiStatus = self.emojiStatus { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId) + ] + } else if let backgroundEmojiId = self.backgroundEmojiId { + return [ + MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId) + ] } else { return nil } @@ -191,7 +204,17 @@ public final class TelegramChannel: Peer, Equatable { public let associatedPeerId: PeerId? = nil public let notificationSettingsPeerId: PeerId? = nil - public var timeoutAttribute: UInt32? { return nil } + public var timeoutAttribute: UInt32? { + if let emojiStatus = self.emojiStatus { + if let expirationDate = emojiStatus.expirationDate { + return UInt32(max(0, expirationDate)) + } else { + return nil + } + } else { + return nil + } + } public init( id: PeerId, @@ -213,7 +236,9 @@ public final class TelegramChannel: Peer, Equatable { nameColor: PeerNameColor?, backgroundEmojiId: Int64?, profileColor: PeerNameColor?, - profileBackgroundEmojiId: Int64? + profileBackgroundEmojiId: Int64?, + emojiStatus: PeerEmojiStatus?, + approximateBoostLevel: Int32? ) { self.id = id self.accessHash = accessHash @@ -235,6 +260,8 @@ public final class TelegramChannel: Peer, Equatable { self.backgroundEmojiId = backgroundEmojiId self.profileColor = profileColor self.profileBackgroundEmojiId = profileBackgroundEmojiId + self.emojiStatus = emojiStatus + self.approximateBoostLevel = approximateBoostLevel } public init(decoder: PostboxDecoder) { @@ -268,6 +295,8 @@ public final class TelegramChannel: Peer, Equatable { self.backgroundEmojiId = decoder.decodeOptionalInt64ForKey("bgem") self.profileColor = decoder.decodeOptionalInt32ForKey("pclr").flatMap { PeerNameColor(rawValue: $0) } self.profileBackgroundEmojiId = decoder.decodeOptionalInt64ForKey("pgem") + self.emojiStatus = decoder.decode(PeerEmojiStatus.self, forKey: "emjs") + self.approximateBoostLevel = decoder.decodeOptionalInt32ForKey("abl") } public func encode(_ encoder: PostboxEncoder) { @@ -347,6 +376,18 @@ public final class TelegramChannel: Peer, Equatable { } else { encoder.encodeNil(forKey: "pgem") } + + if let emojiStatus = self.emojiStatus { + encoder.encode(emojiStatus, forKey: "emjs") + } else { + encoder.encodeNil(forKey: "emjs") + } + + if let approximateBoostLevel = self.approximateBoostLevel { + encoder.encodeInt32(approximateBoostLevel, forKey: "abl") + } else { + encoder.encodeNil(forKey: "abl") + } } public func isEqual(_ other: Peer) -> Bool { @@ -399,43 +440,57 @@ public final class TelegramChannel: Peer, Equatable { if lhs.profileBackgroundEmojiId != rhs.profileBackgroundEmojiId { return false } + if lhs.emojiStatus != rhs.emojiStatus { + return false + } + if lhs.approximateBoostLevel != rhs.approximateBoostLevel { + return false + } return true } public func withUpdatedAddressName(_ addressName: String?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedAddressNames(_ addressNames: [TelegramPeerUsername]) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedDefaultBannedRights(_ defaultBannedRights: TelegramChatBannedRights?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedFlags(_ flags: TelegramChannelFlags) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedNameColor(_ nameColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedProfileColor(_ profileColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) } public func withUpdatedProfileBackgroundEmojiId(_ profileBackgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + } + + public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + } + + public func withUpdatedApproximateBoostLevel(_ approximateBoostLevel: Int32?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 99216285a38..f0668d34e93 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -1,4 +1,5 @@ import Postbox +import Foundation public enum PhoneCallDiscardReason: Int32 { case missed = 0 @@ -73,6 +74,18 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } } + public struct CustomTextAttributes: Equatable { + public var attributes: [(NSRange, NSAttributedString.Key, Any)] + + public init(attributes: [(NSRange, NSAttributedString.Key, Any)]) { + self.attributes = attributes + } + + public static func ==(lhs: CustomTextAttributes, rhs: CustomTextAttributes) -> Bool { + return true + } + } + case unknown case groupCreated(title: String) case addedMembers(peerIds: [PeerId]) @@ -89,7 +102,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case gameScore(gameId: Int64, score: Int32) case phoneCall(callId: Int64, discardReason: PhoneCallDiscardReason?, duration: Int32?, isVideo: Bool) case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool) - case customText(text: String, entities: [MessageTextEntity]) + case customText(text: String, entities: [MessageTextEntity], additionalAttributes: CustomTextAttributes?) case botDomainAccessGranted(domain: String) case botAppAccessGranted(appName: String?, type: BotSendMessageAccessGrantedType?) case botSentSecureValues(types: [SentSecureValueType]) @@ -106,10 +119,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case topicEdited(components: [ForumTopicEditComponent]) case suggestedProfilePhoto(image: TelegramMediaImage?) case attachMenuBotAllowed - case requestedPeer(buttonId: Int32, peerId: PeerId) + case requestedPeer(buttonId: Int32, peerIds: [PeerId]) case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool) case setSameChatWallpaper(wallpaper: TelegramWallpaper) - case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32) + case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) case giveawayLaunched case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32) @@ -152,7 +165,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 15: self = .paymentSent(currency: decoder.decodeStringForKey("currency", orElse: ""), totalAmount: decoder.decodeInt64ForKey("ta", orElse: 0), invoiceSlug: decoder.decodeOptionalStringForKey("invoiceSlug"), isRecurringInit: decoder.decodeBoolForKey("isRecurringInit", orElse: false), isRecurringUsed: decoder.decodeBoolForKey("isRecurringUsed", orElse: false)) case 16: - self = .customText(text: decoder.decodeStringForKey("text", orElse: ""), entities: decoder.decodeObjectArrayWithDecoderForKey("ent")) + self = .customText(text: decoder.decodeStringForKey("text", orElse: ""), entities: decoder.decodeObjectArrayWithDecoderForKey("ent"), additionalAttributes: nil) case 17: self = .botDomainAccessGranted(domain: decoder.decodeStringForKey("do", orElse: "")) case 18: @@ -192,7 +205,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 31: self = .attachMenuBotAllowed case 32: - self = .requestedPeer(buttonId: decoder.decodeInt32ForKey("b", orElse: 0), peerId: PeerId(decoder.decodeInt64ForKey("pi", orElse: 0))) + var peerIds = decoder.decodeInt64ArrayForKey("pis").map { PeerId($0) } + if peerIds.isEmpty { + peerIds = [PeerId(decoder.decodeInt64ForKey("pi", orElse: 0))] + } + self = .requestedPeer(buttonId: decoder.decodeInt32ForKey("b", orElse: 0), peerIds: peerIds) case 33: if let wallpaper = decoder.decode(TelegramWallpaperNativeCodable.self, forKey: "wallpaper")?.value { self = .setChatWallpaper(wallpaper: wallpaper, forBoth: decoder.decodeBoolForKey("both", orElse: false)) @@ -208,7 +225,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 35: self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) case 36: - self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: PeerId(decoder.decodeInt64ForKey("pi", orElse: 0)), months: decoder.decodeInt32ForKey("months", orElse: 0)) + self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) case 37: self = .giveawayLaunched case 38: @@ -298,7 +315,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "d") } encoder.encodeInt32(isVideo ? 1 : 0, forKey: "vc") - case let .customText(text, entities): + case let .customText(text, entities, _): encoder.encodeInt32(16, forKey: "_rawValue") encoder.encodeString(text, forKey: "text") encoder.encodeObjectArray(entities, forKey: "ent") @@ -372,10 +389,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } case .attachMenuBotAllowed: encoder.encodeInt32(31, forKey: "_rawValue") - case let .requestedPeer(buttonId, peerId): + case let .requestedPeer(buttonId, peerIds): encoder.encodeInt32(32, forKey: "_rawValue") encoder.encodeInt32(buttonId, forKey: "b") - encoder.encodeInt64(peerId.toInt64(), forKey: "pi") + encoder.encodeInt64Array(peerIds.map { $0.toInt64() }, forKey: "pis") case let .setChatWallpaper(wallpaper, forBoth): encoder.encodeInt32(33, forKey: "_rawValue") encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper") @@ -395,7 +412,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "atp") } - case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months): + case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount): encoder.encodeInt32(36, forKey: "_rawValue") encoder.encodeString(slug, forKey: "slug") encoder.encodeBool(fromGiveaway, forKey: "give") @@ -406,6 +423,26 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "pi") } encoder.encodeInt32(months, forKey: "months") + if let currency = currency { + encoder.encodeString(currency, forKey: "currency") + } else { + encoder.encodeNil(forKey: "currency") + } + if let amount = amount { + encoder.encodeInt64(amount, forKey: "amount") + } else { + encoder.encodeNil(forKey: "amount") + } + if let cryptoCurrency = cryptoCurrency { + encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency") + } else { + encoder.encodeNil(forKey: "cryptoCurrency") + } + if let cryptoAmount = cryptoAmount { + encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount") + } else { + encoder.encodeNil(forKey: "cryptoAmount") + } case .giveawayLaunched: encoder.encodeInt32(37, forKey: "_rawValue") case .joinedChannel: @@ -433,9 +470,9 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return [from, to] case let .inviteToGroupPhoneCall(_, _, peerIds): return peerIds - case let .requestedPeer(_, peerId): - return [peerId] - case let .giftCode(_, _, _, boostPeerId, _): + case let .requestedPeer(_, peerIds): + return peerIds + case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _): return boostPeerId.flatMap { [$0] } ?? [] default: return [] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift index 7a9ef7e3044..46723467ff5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveaway.swift @@ -9,6 +9,7 @@ public final class TelegramMediaGiveaway: Media, Equatable { } public static let onlyNewSubscribers = Flags(rawValue: 1 << 0) + public static let showWinners = Flags(rawValue: 1 << 1) } public var id: MediaId? { @@ -24,14 +25,16 @@ public final class TelegramMediaGiveaway: Media, Equatable { public let quantity: Int32 public let months: Int32 public let untilDate: Int32 - - public init(flags: Flags, channelPeerIds: [PeerId], countries: [String], quantity: Int32, months: Int32, untilDate: Int32) { + public let prizeDescription: String? + + public init(flags: Flags, channelPeerIds: [PeerId], countries: [String], quantity: Int32, months: Int32, untilDate: Int32, prizeDescription: String?) { self.flags = flags self.channelPeerIds = channelPeerIds self.countries = countries self.quantity = quantity self.months = months self.untilDate = untilDate + self.prizeDescription = prizeDescription } public init(decoder: PostboxDecoder) { @@ -41,6 +44,7 @@ public final class TelegramMediaGiveaway: Media, Equatable { self.quantity = decoder.decodeInt32ForKey("qty", orElse: 0) self.months = decoder.decodeInt32ForKey("mts", orElse: 0) self.untilDate = decoder.decodeInt32ForKey("unt", orElse: 0) + self.prizeDescription = decoder.decodeOptionalStringForKey("des") } public func encode(_ encoder: PostboxEncoder) { @@ -50,6 +54,11 @@ public final class TelegramMediaGiveaway: Media, Equatable { encoder.encodeInt32(self.quantity, forKey: "qty") encoder.encodeInt32(self.months, forKey: "mts") encoder.encodeInt32(self.untilDate, forKey: "unt") + if let prizeDescription = self.prizeDescription { + encoder.encodeString(prizeDescription, forKey: "des") + } else { + encoder.encodeNil(forKey: "des") + } } public func isLikelyToBeUpdated() -> Bool { @@ -78,6 +87,9 @@ public final class TelegramMediaGiveaway: Media, Equatable { if self.untilDate != other.untilDate { return false } + if self.prizeDescription != other.prizeDescription { + return false + } return true } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveawayResults.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveawayResults.swift new file mode 100644 index 00000000000..1e00f83c1b9 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaGiveawayResults.swift @@ -0,0 +1,118 @@ +import Postbox + +public final class TelegramMediaGiveawayResults: Media, Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let refunded = Flags(rawValue: 1 << 0) + public static let onlyNewSubscribers = Flags(rawValue: 1 << 1) + } + + public var id: MediaId? { + return nil + } + public var peerIds: [PeerId] { + return self.winnersPeerIds + } + + public let flags: Flags + public let launchMessageId: MessageId + public let additionalChannelsCount: Int32 + public let winnersPeerIds: [PeerId] + public let winnersCount: Int32 + public let unclaimedCount: Int32 + public let months: Int32 + public let untilDate: Int32 + public let prizeDescription: String? + + public init(flags: Flags, launchMessageId: MessageId, additionalChannelsCount: Int32, winnersPeerIds: [PeerId], winnersCount: Int32, unclaimedCount: Int32, months: Int32, untilDate: Int32, prizeDescription: String?) { + self.flags = flags + self.launchMessageId = launchMessageId + self.additionalChannelsCount = additionalChannelsCount + self.winnersPeerIds = winnersPeerIds + self.winnersCount = winnersCount + self.unclaimedCount = unclaimedCount + self.months = months + self.untilDate = untilDate + self.prizeDescription = prizeDescription + } + + public init(decoder: PostboxDecoder) { + self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flg", orElse: 0)) + self.launchMessageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("msgp", orElse: 0)), namespace: Namespaces.Message.Cloud, id: decoder.decodeInt32ForKey("msgi", orElse: 0)) + self.additionalChannelsCount = decoder.decodeInt32ForKey("chn", orElse: 0) + self.winnersPeerIds = decoder.decodeInt64ArrayForKey("wnr").map { PeerId($0) } + self.winnersCount = decoder.decodeInt32ForKey("wnc", orElse: 0) + self.unclaimedCount = decoder.decodeInt32ForKey("unc", orElse: 0) + self.months = decoder.decodeInt32ForKey("mts", orElse: 0) + self.untilDate = decoder.decodeInt32ForKey("unt", orElse: 0) + self.prizeDescription = decoder.decodeOptionalStringForKey("des") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.flags.rawValue, forKey: "flg") + encoder.encodeInt64(self.launchMessageId.peerId.toInt64(), forKey: "msgp") + encoder.encodeInt32(self.launchMessageId.id, forKey: "msgi") + encoder.encodeInt32(self.additionalChannelsCount, forKey: "chn") + encoder.encodeInt64Array(self.winnersPeerIds.map { $0.toInt64() }, forKey: "wnr") + encoder.encodeInt32(self.winnersCount, forKey: "wnc") + encoder.encodeInt32(self.unclaimedCount, forKey: "unc") + encoder.encodeInt32(self.months, forKey: "mts") + encoder.encodeInt32(self.untilDate, forKey: "unt") + if let prizeDescription = self.prizeDescription { + encoder.encodeString(prizeDescription, forKey: "des") + } else { + encoder.encodeNil(forKey: "des") + } + } + + public func isLikelyToBeUpdated() -> Bool { + return false + } + + public func isEqual(to other: Media) -> Bool { + guard let other = other as? TelegramMediaGiveawayResults else { + return false + } + if self.flags != other.flags { + return false + } + if self.launchMessageId != other.launchMessageId { + return false + } + if self.additionalChannelsCount != other.additionalChannelsCount { + return false + } + if self.winnersPeerIds != other.winnersPeerIds { + return false + } + if self.winnersCount != other.winnersCount { + return false + } + if self.unclaimedCount != other.unclaimedCount { + return false + } + if self.months != other.months { + return false + } + if self.untilDate != other.untilDate { + return false + } + if self.prizeDescription != other.prizeDescription { + return false + } + return true + } + + public func isSemanticallyEqual(to other: Media) -> Bool { + return self.isEqual(to: other) + } + + public static func ==(lhs: TelegramMediaGiveawayResults, rhs: TelegramMediaGiveawayResults) -> Bool { + return lhs.isEqual(to: rhs) + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift index 3645af6204c..5c0518226e5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift @@ -6,13 +6,15 @@ public struct WallpaperSettings: Codable, Equatable { public var colors: [UInt32] public var intensity: Int32? public var rotation: Int32? + public var emoticon: String? - public init(blur: Bool = false, motion: Bool = false, colors: [UInt32] = [], intensity: Int32? = nil, rotation: Int32? = nil) { + public init(blur: Bool = false, motion: Bool = false, colors: [UInt32] = [], intensity: Int32? = nil, rotation: Int32? = nil, emoticon: String? = nil) { self.blur = blur self.motion = motion self.colors = colors self.intensity = intensity self.rotation = rotation + self.emoticon = emoticon } public init(from decoder: Decoder) throws { @@ -32,6 +34,7 @@ public struct WallpaperSettings: Codable, Equatable { self.intensity = try container.decodeIfPresent(Int32.self, forKey: "i") self.rotation = try container.decodeIfPresent(Int32.self, forKey: "r") + self.emoticon = try container.decodeIfPresent(String.self, forKey: "e") } public func encode(to encoder: Encoder) throws { @@ -42,6 +45,7 @@ public struct WallpaperSettings: Codable, Equatable { try container.encode(self.colors.map(Int32.init(bitPattern:)), forKey: "colors") try container.encodeIfPresent(self.intensity, forKey: "i") try container.encodeIfPresent(self.rotation, forKey: "r") + try container.encodeIfPresent(self.emoticon, forKey: "e") } public static func ==(lhs: WallpaperSettings, rhs: WallpaperSettings) -> Bool { @@ -60,6 +64,9 @@ public struct WallpaperSettings: Codable, Equatable { if lhs.rotation != rhs.rotation { return false } + if lhs.emoticon != rhs.emoticon { + return false + } return true } } @@ -73,7 +80,7 @@ public struct TelegramWallpaperNativeCodable: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - + switch try container.decode(Int32.self, forKey: "v") { case 0: let settings = try container.decode(WallpaperSettings.self, forKey: "settings") @@ -106,9 +113,9 @@ public struct TelegramWallpaperNativeCodable: Codable { } case 4: let settings = try container.decode(WallpaperSettings.self, forKey: "settings") - + var colors: [UInt32] = [] - + if let topColor = (try container.decodeIfPresent(Int32.self, forKey: "c1")).flatMap(UInt32.init(bitPattern:)) { colors.append(topColor) if let bottomColor = (try container.decodeIfPresent(Int32.self, forKey: "c2")).flatMap(UInt32.init(bitPattern:)) { @@ -117,12 +124,14 @@ public struct TelegramWallpaperNativeCodable: Codable { } else { colors = (try container.decode([Int32].self, forKey: "colors")).map(UInt32.init(bitPattern:)) } - + self.value = .gradient(TelegramWallpaper.Gradient( id: try container.decodeIfPresent(Int64.self, forKey: "id"), colors: colors, settings: settings )) + case 5: + self.value = .emoticon(try container.decode(String.self, forKey: "e")) default: assertionFailure() self.value = .color(0xffffff) @@ -131,41 +140,48 @@ public struct TelegramWallpaperNativeCodable: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - + switch self.value { - case let .builtin(settings): - try container.encode(0 as Int32, forKey: "v") - try container.encode(settings, forKey: "settings") - case let .color(color): - try container.encode(1 as Int32, forKey: "v") - try container.encode(Int32(bitPattern: color), forKey: "c") - case let .gradient(gradient): - try container.encode(4 as Int32, forKey: "v") - try container.encodeIfPresent(gradient.id, forKey: "id") - try container.encode(gradient.colors.map(Int32.init(bitPattern:)), forKey: "colors") - try container.encode(gradient.settings, forKey: "settings") - case let .image(representations, settings): - try container.encode(2 as Int32, forKey: "v") - try container.encode(representations.map { item in - return PostboxEncoder().encodeObjectToRawData(item) - }, forKey: "i") - try container.encode(settings, forKey: "settings") - case let .file(file): - try container.encode(3 as Int32, forKey: "v") - try container.encode(file.id, forKey: "id") - try container.encode(file.accessHash, forKey: "accessHash") - try container.encode((file.isCreator ? 1 : 0) as Int32, forKey: "isCreator") - try container.encode((file.isDefault ? 1 : 0) as Int32, forKey: "isDefault") - try container.encode((file.isPattern ? 1 : 0) as Int32, forKey: "isPattern") - try container.encode((file.isDark ? 1 : 0) as Int32, forKey: "isDark") - try container.encode(file.slug, forKey: "slug") - try container.encode(PostboxEncoder().encodeObjectToRawData(file.file), forKey: "file") - try container.encode(file.settings, forKey: "settings") + case let .builtin(settings): + try container.encode(0 as Int32, forKey: "v") + try container.encode(settings, forKey: "settings") + case let .color(color): + try container.encode(1 as Int32, forKey: "v") + try container.encode(Int32(bitPattern: color), forKey: "c") + case let .gradient(gradient): + try container.encode(4 as Int32, forKey: "v") + try container.encodeIfPresent(gradient.id, forKey: "id") + try container.encode(gradient.colors.map(Int32.init(bitPattern:)), forKey: "colors") + try container.encode(gradient.settings, forKey: "settings") + case let .image(representations, settings): + try container.encode(2 as Int32, forKey: "v") + try container.encode(representations.map { item in + return PostboxEncoder().encodeObjectToRawData(item) + }, forKey: "i") + try container.encode(settings, forKey: "settings") + case let .file(file): + try container.encode(3 as Int32, forKey: "v") + try container.encode(file.id, forKey: "id") + try container.encode(file.accessHash, forKey: "accessHash") + try container.encode((file.isCreator ? 1 : 0) as Int32, forKey: "isCreator") + try container.encode((file.isDefault ? 1 : 0) as Int32, forKey: "isDefault") + try container.encode((file.isPattern ? 1 : 0) as Int32, forKey: "isPattern") + try container.encode((file.isDark ? 1 : 0) as Int32, forKey: "isDark") + try container.encode(file.slug, forKey: "slug") + try container.encode(PostboxEncoder().encodeObjectToRawData(file.file), forKey: "file") + try container.encode(file.settings, forKey: "settings") + case let .emoticon(emoticon): + try container.encode(5 as Int32, forKey: "v") + try container.encode(emoticon, forKey: "e") } } } public enum TelegramWallpaper: Equatable { + public static func emoticonWallpaper(emoticon: String) -> TelegramWallpaper { + return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(emoticon: emoticon))) + } + public struct Gradient: Equatable { public var id: Int64? public var colors: [UInt32] @@ -221,6 +237,7 @@ public enum TelegramWallpaper: Equatable { case gradient(Gradient) case image([TelegramMediaImageRepresentation], WallpaperSettings) case file(File) + case emoticon(String) public var hasWallpaper: Bool { switch self { @@ -263,6 +280,12 @@ public enum TelegramWallpaper: Equatable { } else { return false } + case let .emoticon(emoticon): + if case .emoticon(emoticon) = rhs { + return true + } else { + return false + } } } @@ -298,6 +321,12 @@ public enum TelegramWallpaper: Equatable { } else { return false } + case let .emoticon(emoticon): + if case .emoticon(emoticon) = wallpaper { + return true + } else { + return false + } } } @@ -328,6 +357,8 @@ public enum TelegramWallpaper: Equatable { case var .file(file): file.settings = settings return .file(file) + case .emoticon: + return self } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 9e11ce4fca8..1531576a483 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2254,7 +2254,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network: for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 7e4ebeff10a..fbe82f18c49 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -318,6 +318,40 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct Wallpaper: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + guard let cachedPeerData = view.cachedPeerData else { + return nil + } + switch cachedPeerData { + case let user as CachedUserData: + return user.wallpaper + case let channel as CachedChannelData: + return channel.wallpaper + default: + return nil + } + } + } public struct GroupCallDescription: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Optional diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 493c924e6ce..f35211a3e98 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -315,7 +315,9 @@ private class AdMessagesHistoryContextImpl { nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, - profileBackgroundEmojiId: nil + profileBackgroundEmojiId: nil, + emojiStatus: nil, + approximateBoostLevel: nil ) case let .webPage(webPage): author = TelegramChannel( @@ -338,7 +340,9 @@ private class AdMessagesHistoryContextImpl { nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, - profileBackgroundEmojiId: nil + profileBackgroundEmojiId: nil, + emojiStatus: nil, + approximateBoostLevel: nil ) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 635c340d9e9..a57ff9e065a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -19,47 +19,182 @@ public final class EngineStoryViewListContext { case recentFirst } - public final class Item: Equatable { - public let peer: EnginePeer - public let timestamp: Int32 - public let storyStats: PeerStoryStats? - public let reaction: MessageReaction.Reaction? - public let reactionFile: TelegramMediaFile? + + public enum Item: Equatable { + public final class View: Equatable { + public let peer: EnginePeer + public let timestamp: Int32 + public let storyStats: PeerStoryStats? + public let reaction: MessageReaction.Reaction? + public let reactionFile: TelegramMediaFile? + + public init( + peer: EnginePeer, + timestamp: Int32, + storyStats: PeerStoryStats?, + reaction: MessageReaction.Reaction?, + reactionFile: TelegramMediaFile? + ) { + self.peer = peer + self.timestamp = timestamp + self.storyStats = storyStats + self.reaction = reaction + self.reactionFile = reactionFile + } + + public static func ==(lhs: View, rhs: View) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.storyStats != rhs.storyStats { + return false + } + if lhs.reaction != rhs.reaction { + return false + } + if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId { + return false + } + return true + } + } - public init( - peer: EnginePeer, - timestamp: Int32, - storyStats: PeerStoryStats?, - reaction: MessageReaction.Reaction?, - reactionFile: TelegramMediaFile? - ) { - self.peer = peer - self.timestamp = timestamp - self.storyStats = storyStats - self.reaction = reaction - self.reactionFile = reactionFile + public final class Repost: Equatable { + public let peer: EnginePeer + public let story: EngineStoryItem + public let storyStats: PeerStoryStats? + + init(peer: EnginePeer, story: EngineStoryItem, storyStats: PeerStoryStats?) { + self.peer = peer + self.story = story + self.storyStats = storyStats + } + + public static func ==(lhs: Repost, rhs: Repost) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.story != rhs.story { + return false + } + if lhs.storyStats != rhs.storyStats { + return false + } + return true + } } - public static func ==(lhs: Item, rhs: Item) -> Bool { - if lhs.peer != rhs.peer { - return false + public final class Forward: Equatable { + public let message: EngineMessage + public let storyStats: PeerStoryStats? + + init(message: EngineMessage, storyStats: PeerStoryStats?) { + self.message = message + self.storyStats = storyStats } - if lhs.timestamp != rhs.timestamp { - return false + + public static func ==(lhs: Forward, rhs: Forward) -> Bool { + if lhs.message != rhs.message { + return false + } + if lhs.storyStats != rhs.storyStats { + return false + } + return true } - if lhs.storyStats != rhs.storyStats { - return false + } + + case view(View) + case repost(Repost) + case forward(Forward) + + public var peer: EnginePeer { + switch self { + case let .view(view): + return view.peer + case let .repost(repost): + return repost.peer + case let .forward(forward): + return EnginePeer(forward.message.peers[forward.message.id.peerId]!) } - if lhs.reaction != rhs.reaction { - return false + } + + public var timestamp: Int32 { + switch self { + case let .view(view): + return view.timestamp + case let .repost(repost): + return repost.story.timestamp + case let .forward(forward): + return forward.message.timestamp } - if lhs.reactionFile?.fileId != rhs.reactionFile?.fileId { - return false + } + + public var reaction: MessageReaction.Reaction? { + switch self { + case let .view(view): + return view.reaction + case .repost: + return nil + case .forward: + return nil + } + } + + public var story: EngineStoryItem? { + switch self { + case .view: + return nil + case let .repost(repost): + return repost.story + case .forward: + return nil + } + } + + public var message: EngineMessage? { + switch self { + case .view: + return nil + case .repost: + return nil + case let .forward(forward): + return forward.message + } + } + + public var storyStats: PeerStoryStats? { + switch self { + case let .view(view): + return view.storyStats + case let .repost(repost): + return repost.storyStats + case let .forward(forward): + return forward.storyStats + } + } + + public struct ItemHash: Hashable { + public var peerId: EnginePeer.Id + public var storyId: Int32? + public var messageId: EngineMessage.Id? + } + + public var uniqueId: ItemHash { + switch self { + case let .view(view): + return ItemHash(peerId: view.peer.id, storyId: nil, messageId: nil) + case let .repost(repost): + return ItemHash(peerId: repost.peer.id, storyId: repost.story.id, messageId: nil) + case let .forward(forward): + return ItemHash(peerId: forward.message.id.peerId, storyId: nil, messageId: forward.message.id) } - return true } } - + public struct State: Equatable { public var totalCount: Int public var totalReactedCount: Int @@ -86,6 +221,8 @@ public final class EngineStoryViewListContext { struct InternalState: Equatable { var totalCount: Int + var totalViewsCount: Int + var totalForwardsCount: Int var totalReactedCount: Int var items: [Item] var canLoadMore: Bool @@ -179,8 +316,11 @@ public final class EngineStoryViewListContext { switch sortMode { case .repostsFirst: items.sort(by: { lhs, rhs in - if (lhs.reaction == nil) != (rhs.reaction == nil) { - return lhs.reaction != nil + if (lhs.story == nil) != (rhs.story == nil) { + return lhs.story != nil + } + if (lhs.message == nil) != (rhs.message == nil) { + return lhs.message != nil } if lhs.timestamp != rhs.timestamp { return lhs.timestamp > rhs.timestamp @@ -189,6 +329,12 @@ public final class EngineStoryViewListContext { }) case .reactionsFirst: items.sort(by: { lhs, rhs in + if (lhs.story == nil) != (rhs.story == nil) { + return lhs.story == nil + } + if (lhs.message == nil) != (rhs.message == nil) { + return lhs.message == nil + } if (lhs.reaction == nil) != (rhs.reaction == nil) { return lhs.reaction != nil } @@ -221,6 +367,8 @@ public final class EngineStoryViewListContext { return InternalState( totalCount: totalCount, + totalViewsCount: 0, + totalForwardsCount: 0, totalReactedCount: totalReactedCount, items: items, canLoadMore: state.canLoadMore @@ -236,7 +384,7 @@ public final class EngineStoryViewListContext { })) } else { let initialState = State(totalCount: listMode == .everyone ? views.seenCount : 100, totalReactedCount: views.reactedCount, items: [], loadMoreToken: LoadMoreToken(value: "")) - let state = InternalState(totalCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) + let state = InternalState(totalCount: initialState.totalCount, totalViewsCount: initialState.totalCount, totalForwardsCount: initialState.totalCount, totalReactedCount: initialState.totalReactedCount, items: initialState.items, canLoadMore: initialState.loadMoreToken != nil, nextOffset: nil) self.state = state self.statePromise.set(.single(state)) @@ -279,167 +427,339 @@ public final class EngineStoryViewListContext { let sortMode = self.sortMode let searchQuery = self.searchQuery let currentOffset = state.nextOffset - let limit = state.items.isEmpty ? 50 : 100 - let signal: Signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - } - |> mapToSignal { inputPeer -> Signal in - guard let inputPeer = inputPeer else { - return .complete() - } - - var flags: Int32 = 0 - switch listMode { - case .everyone: - break - case .contacts: - flags |= (1 << 0) - } - switch sortMode { - case .reactionsFirst: - flags |= (1 << 2) - case .recentFirst, .repostsFirst: - break - } - if searchQuery != nil { - flags |= (1 << 1) - } - - return account.network.request(Api.functions.stories.getStoryViewsList(flags: flags, peer: inputPeer, q: searchQuery, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit))) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + let limit = 50 + + let signal: Signal + + if peerId.namespace == Namespaces.Peer.CloudUser { + signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> InternalState in - switch result { - case let .storyViewsList(_, count, reactionsCount, views, users, nextOffset): - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) - - var items: [Item] = [] - for view in views { - switch view { - case let .storyView(flags, userId, date, reaction): - let isBlocked = (flags & (1 << 0)) != 0 - let isBlockedFromStories = (flags & (1 << 1)) != 0 - - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in - let previousData: CachedUserData - if let current = cachedData as? CachedUserData { - previousData = current - } else { - previousData = CachedUserData() + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .complete() + } + + var flags: Int32 = 0 + switch listMode { + case .everyone: + break + case .contacts: + flags |= (1 << 0) + } + switch sortMode { + case .reactionsFirst: + flags |= (1 << 2) + case .recentFirst, .repostsFirst: + break + } + if searchQuery != nil { + flags |= (1 << 1) + } + + return account.network.request(Api.functions.stories.getStoryViewsList(flags: flags, peer: inputPeer, q: searchQuery, id: storyId, offset: currentOffset?.value ?? "", limit: Int32(limit))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> InternalState in + switch result { + case let .storyViewsList(_, count, viewsCount, forwardsCount, reactionsCount, views, chats, users, nextOffset): + let peers = AccumulatedPeers(chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers) + + var items: [Item] = [] + for view in views { + switch view { + case let .storyView(flags, userId, date, reaction): + let isBlocked = (flags & (1 << 0)) != 0 + let isBlockedFromStories = (flags & (1 << 1)) != 0 + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in + let previousData: CachedUserData + if let current = cachedData as? CachedUserData { + previousData = current + } else { + previousData = CachedUserData() + } + var updatedFlags = previousData.flags + if isBlockedFromStories { + updatedFlags.insert(.isBlockedFromStories) + } else { + updatedFlags.remove(.isBlockedFromStories) + } + return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) + }) + if let peer = transaction.getPeer(peerId) { + let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) + items.append(.view(Item.View( + peer: EnginePeer(peer), + timestamp: date, + storyStats: transaction.getPeerStoryStats(peerId: peerId), + reaction: parsedReaction, + reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in + switch reaction { + case .builtin: + return nil + case let .custom(fileId): + return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + } + } + ))) } - var updatedFlags = previousData.flags - if isBlockedFromStories { - updatedFlags.insert(.isBlockedFromStories) - } else { - updatedFlags.remove(.isBlockedFromStories) + case let .storyViewPublicForward(flags, message): + let _ = flags + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) { + items.append(.forward(Item.Forward( + message: EngineMessage(message), + storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId) + ))) } - return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) - }) - if let peer = transaction.getPeer(peerId) { - let parsedReaction = reaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) - items.append(Item( - peer: EnginePeer(peer), - timestamp: date, - storyStats: transaction.getPeerStoryStats(peerId: peerId), - reaction: parsedReaction, - reactionFile: parsedReaction.flatMap { reaction -> TelegramMediaFile? in - switch reaction { - case .builtin: - return nil - case let .custom(fileId): - return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile - } + case let .storyViewPublicRepost(flags, peerId, story): + let _ = flags + if let peer = transaction.getPeer(peerId.peerId) { + if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media { + items.append(.repost(Item.Repost( + peer: EnginePeer(peer), + story: EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + }, + reactions: views.reactions, + hasList: views.hasList + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) } + ), + storyStats: transaction.getPeerStoryStats(peerId: peer.id) + ))) } - )) + } } } - } - - if listMode == .everyone, searchQuery == nil { - if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views { - let updatedItem: Stories.StoredItem = .item(Stories.Item( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: item.media, - mediaAreas: item.mediaAreas, - text: item.text, - entities: item.entities, - views: Stories.Item.Views( - seenCount: Int(count), - reactedCount: Int(reactionsCount), - forwardCount: currentViews.forwardCount, - seenPeerIds: currentViews.seenPeerIds, - reactions: currentViews.reactions, - hasList: currentViews.hasList - ), - privacy: item.privacy, - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends, - isContacts: item.isContacts, - isSelectedContacts: item.isSelectedContacts, - isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited, - isMy: item.isMy, - myReaction: item.myReaction, - forwardInfo: item.forwardInfo - )) - if let entry = CodableEntry(updatedItem) { - transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) + + if listMode == .everyone, searchQuery == nil { + if let storedItem = transaction.getStory(id: StoryId(peerId: account.peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(item) = storedItem, let currentViews = item.views { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + alternativeMedia: item.alternativeMedia, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: Stories.Item.Views( + seenCount: Int(count), + reactedCount: Int(reactionsCount), + forwardCount: Int(forwardsCount), + seenPeerIds: currentViews.seenPeerIds, + reactions: currentViews.reactions, + hasList: currentViews.hasList + ), + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo + )) + if let entry = CodableEntry(updatedItem) { + transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) + } } + + var currentItems = transaction.getStoryItems(peerId: account.peerId) + for i in 0 ..< currentItems.count { + if currentItems[i].id == storyId { + if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + alternativeMedia: item.alternativeMedia, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: Stories.Item.Views( + seenCount: Int(count), + reactedCount: Int(reactionsCount), + forwardCount: Int(forwardsCount), + seenPeerIds: currentViews.seenPeerIds, + reactions: currentViews.reactions, + hasList: currentViews.hasList + ), + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo + )) + if let entry = CodableEntry(updatedItem) { + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) + } + } + } + } + transaction.setStoryItems(peerId: account.peerId, items: currentItems) } - var currentItems = transaction.getStoryItems(peerId: account.peerId) - for i in 0 ..< currentItems.count { - if currentItems[i].id == storyId { - if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self), let currentViews = item.views { - let updatedItem: Stories.StoredItem = .item(Stories.Item( - id: item.id, - timestamp: item.timestamp, - expirationTimestamp: item.expirationTimestamp, - media: item.media, - mediaAreas: item.mediaAreas, - text: item.text, - entities: item.entities, - views: Stories.Item.Views( - seenCount: Int(count), - reactedCount: Int(reactionsCount), - forwardCount: currentViews.forwardCount, - seenPeerIds: currentViews.seenPeerIds, - reactions: currentViews.reactions, - hasList: currentViews.hasList - ), - privacy: item.privacy, - isPinned: item.isPinned, - isExpired: item.isExpired, - isPublic: item.isPublic, - isCloseFriends: item.isCloseFriends, - isContacts: item.isContacts, - isSelectedContacts: item.isSelectedContacts, - isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited, - isMy: item.isMy, - myReaction: item.myReaction, - forwardInfo: item.forwardInfo - )) - if let entry = CodableEntry(updatedItem) { - currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) + return InternalState(totalCount: Int(count), totalViewsCount: Int(viewsCount), totalForwardsCount: Int(forwardsCount), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) }) + case .none: + return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) + } + } + } + } + } else { + signal = self.account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer = inputPeer else { + return .complete() + } + + var flags: Int32 = 0 + if let _ = currentOffset { + flags |= (1 << 1) + } + if case .repostsFirst = sortMode { + flags |= (1 << 2) + } + + return account.network.request(Api.functions.stories.getStoryReactionsList(flags: flags, peer: inputPeer, id: storyId, reaction: nil, offset: currentOffset?.value, limit: Int32(limit))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> InternalState in + switch result { + case let .storyReactionsList(_, count, reactions, chats, users, nextOffset): + let peers = AccumulatedPeers(chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers) + + var items: [Item] = [] + for reaction in reactions { + switch reaction { + case let .storyReaction(peerId, date, reaction): + if let peer = transaction.getPeer(peerId.peerId) { + if let parsedReaction = MessageReaction.Reaction(apiReaction: reaction) { + let reactionFile: TelegramMediaFile? + switch parsedReaction { + case .builtin: + reactionFile = nil + case let .custom(fileId): + reactionFile = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + } + items.append(.view(Item.View( + peer: EnginePeer(peer), + timestamp: date, + storyStats: transaction.getPeerStoryStats(peerId: peer.id), + reaction: parsedReaction, + reactionFile: reactionFile + ))) + } + } + case let .storyReactionPublicForward(message): + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: false), let message = locallyRenderedMessage(message: storeMessage, peers: peers.peers) { + items.append(.forward(Item.Forward( + message: EngineMessage(message), + storyStats: transaction.getPeerStoryStats(peerId: message.id.peerId) + ))) + } + case let .storyReactionPublicRepost(peerId, story): + if let peer = transaction.getPeer(peerId.peerId) { + if let storedItem = Stories.StoredItem(apiStoryItem: story, peerId: peer.id, transaction: transaction), case let .item(item) = storedItem, let media = item.media { + items.append(.repost(Item.Repost( + peer: EnginePeer(peer), + story: EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + reactedCount: views.reactedCount, + forwardCount: views.forwardCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + }, + reactions: views.reactions, + hasList: views.hasList + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + isMy: item.isMy, + myReaction: item.myReaction, + forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) } + ), + storyStats: transaction.getPeerStoryStats(peerId: peer.id) + ))) } } } } - transaction.setStoryItems(peerId: account.peerId, items: currentItems) + return InternalState(totalCount: Int(count), totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: Int(count), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) }) + case .none: + return InternalState(totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) } - - return InternalState(totalCount: Int(count), totalReactedCount: Int(reactionsCount), items: items, canLoadMore: nextOffset != nil, nextOffset: nextOffset.flatMap { NextOffset(value: $0) }) - case .none: - return InternalState(totalCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) } } } @@ -455,23 +775,19 @@ public final class EngineStoryViewListContext { private func updateInternalState(state: InternalState) { var currentState = self.state ?? InternalState( - totalCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) - - struct ItemHash: Hashable { - var peerId: EnginePeer.Id - } + totalCount: 0, totalViewsCount: 0, totalForwardsCount: 0, totalReactedCount: 0, items: [], canLoadMore: false, nextOffset: nil) if self.parentSource != nil { currentState.items.removeAll() } - var existingItems = Set() + var existingItems = Set() for item in currentState.items { - existingItems.insert(ItemHash(peerId: item.peer.id)) + existingItems.insert(item.uniqueId) } for item in state.items { - let itemHash = ItemHash(peerId: item.peer.id) + let itemHash = item.uniqueId if existingItems.contains(itemHash) { continue } @@ -481,7 +797,7 @@ public final class EngineStoryViewListContext { var allReactedCount = 0 for item in currentState.items { - if item.reaction != nil { + if case let .view(view) = item, view.reaction != nil { allReactedCount += 1 } else { break @@ -516,15 +832,15 @@ public final class EngineStoryViewListContext { for i in 0 ..< state.items.count { let item = items[i] let value = view.storyStats[item.peer.id] - if item.storyStats != value { + if case let .view(view) = item, view.storyStats != value { updated = true - items[i] = Item( - peer: item.peer, - timestamp: item.timestamp, + items[i] = .view(Item.View( + peer: view.peer, + timestamp: view.timestamp, storyStats: value, - reaction: item.reaction, - reactionFile: item.reactionFile - ) + reaction: view.reaction, + reactionFile: view.reactionFile + )) } } if updated { @@ -574,4 +890,3 @@ public final class EngineStoryViewListContext { } } } - diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift index 0d8ecdba8e2..6d54271de45 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift @@ -14,7 +14,11 @@ public enum GetMessagesStrategy { case cloud(skipLocal: Bool) } -func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal { +public enum GetMessagesError { + case privateChannel +} + +func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal { let postboxSignal = postbox.transaction { transaction -> ([Message], Set, SimpleDictionary) in var ids = messageIds @@ -50,8 +54,9 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po if case .cloud = strategy { return postboxSignal + |> castError(GetMessagesError.self) |> mapToSignal { (existMessages, missingMessageIds, supportPeers) in - var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), NoError>] = [] + var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>] = [] for (peerId, messageIds) in messagesIdsGroupedByPeerId(missingMessageIds) { if let peer = supportPeers[peerId] { var signal: Signal? @@ -75,21 +80,27 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po case .messagesNotModified: return (peer, [], [], []) } - } |> `catch` { _ in - return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), NoError>.single((peer, [], [], [])) + } |> `catch` { error in + if error.errorDescription == "CHANNEL_PRIVATE" { + return .fail(.privateChannel) + } else { + return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>.single((peer, [], [], [])) + } }) } } } - return .single(.progress) |> then(combineLatest(signals) |> mapToSignal { results -> Signal in + return .single(.progress) + |> castError(GetMessagesError.self) + |> then(combineLatest(signals) |> mapToSignal { results -> Signal in return postbox.transaction { transaction -> GetMessagesResult in for (peer, messages, chats, users) in results { if !messages.isEmpty { var storeMessages: [StoreMessage] = [] for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum) { storeMessages.append(message) } } @@ -108,10 +119,12 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po return .result(existMessages + loadedMessages) } + |> castError(GetMessagesError.self) }) } } else { return postboxSignal + |> castError(GetMessagesError.self) |> map { return .result($0.0) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift index ca46e3dc8bc..8d5044b6dcf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Media.swift @@ -18,6 +18,7 @@ public enum EngineMedia: Equatable { case webpage(TelegramMediaWebpage) case story(TelegramMediaStory) case giveaway(TelegramMediaGiveaway) + case giveawayResults(TelegramMediaGiveawayResults) } public extension EngineMedia { @@ -53,6 +54,8 @@ public extension EngineMedia { return story.id case let .giveaway(giveaway): return giveaway.id + case let .giveawayResults(giveawayResults): + return giveawayResults.id } } } @@ -90,6 +93,8 @@ public extension EngineMedia { self = .story(story) case let giveaway as TelegramMediaGiveaway: self = .giveaway(giveaway) + case let giveawayResults as TelegramMediaGiveawayResults: + self = .giveawayResults(giveawayResults) default: preconditionFailure() } @@ -127,6 +132,8 @@ public extension EngineMedia { return story case let .giveaway(giveaway): return giveaway + case let .giveawayResults(giveawayResults): + return giveawayResults } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index 6cee707abbb..7f810cccacc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -124,6 +124,7 @@ public enum MediaArea: Codable, Equatable { case venue(coordinates: Coordinates, venue: Venue) case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) + case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -144,6 +145,7 @@ public enum MediaArea: Codable, Equatable { private enum MediaAreaType: Int32 { case venue case reaction + case channelMessage } public enum DecodingError: Error { @@ -166,6 +168,10 @@ public enum MediaArea: Codable, Equatable { let reaction = try container.decode(MessageReaction.Reaction.self, forKey: .value) let flags = ReactionFlags(rawValue: try container.decodeIfPresent(Int32.self, forKey: .flags) ?? 0) self = .reaction(coordinates: coordinates, reaction: reaction, flags: flags) + case .channelMessage: + let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) + let messageId = try container.decode(MessageId.self, forKey: .value) + self = .channelMessage(coordinates: coordinates, messageId: messageId) } } @@ -182,6 +188,10 @@ public enum MediaArea: Codable, Equatable { try container.encode(coordinates, forKey: .coordinates) try container.encode(reaction, forKey: .value) try container.encode(flags.rawValue, forKey: .flags) + case let .channelMessage(coordinates, messageId): + try container.encode(MediaAreaType.channelMessage.rawValue, forKey: .type) + try container.encode(coordinates, forKey: .coordinates) + try container.encode(messageId, forKey: .value) } } } @@ -193,6 +203,8 @@ public extension MediaArea { return coordinates case let .reaction(coordinates, _, _): return coordinates + case let .channelMessage(coordinates, _): + return coordinates } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index cb822f12047..a55459cb39d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -161,7 +161,7 @@ private class ReplyThreadHistoryContextImpl { switch discussionMessage { case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users): let parsedMessages = messages.compactMap { message -> StoreMessage? in - StoreMessage(apiMessage: message, peerIsForum: peer.isForum) + StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum) } guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else { @@ -555,6 +555,7 @@ public struct ChatReplyThreadMessage: Equatable { } public var messageId: MessageId + public var threadId: Int64 public var channelMessageId: MessageId? public var isChannelPost: Bool public var isForumPost: Bool @@ -566,8 +567,9 @@ public struct ChatReplyThreadMessage: Equatable { public var initialAnchor: Anchor public var isNotAvailable: Bool - public init(messageId: MessageId, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) { + public init(messageId: MessageId, threadId: Int64, channelMessageId: MessageId?, isChannelPost: Bool, isForumPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, unreadCount: Int, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) { self.messageId = messageId + self.threadId = threadId self.channelMessageId = channelMessageId self.isChannelPost = isChannelPost self.isForumPost = isForumPost @@ -582,7 +584,7 @@ public struct ChatReplyThreadMessage: Equatable { public var normalized: ChatReplyThreadMessage { if self.isForumPost { - return ChatReplyThreadMessage(messageId: self.messageId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) + return ChatReplyThreadMessage(messageId: self.messageId, threadId: self.threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false) } else { return self } @@ -626,7 +628,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa switch discussionMessage { case let .discussionMessage(_, messages, maxId, readInboxMaxId, readOutboxMaxId, unreadCount, chats, users): let parsedMessages = messages.compactMap { message -> StoreMessage? in - StoreMessage(apiMessage: message, peerIsForum: peer.isForum) + StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum) } guard let topMessage = parsedMessages.last, let parsedIndex = topMessage.index else { @@ -936,6 +938,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa return .single(ChatReplyThreadMessage( messageId: discussionMessage.messageId, + threadId: Int64(discussionMessage.messageId.id), channelMessageId: discussionMessage.channelMessageId, isChannelPost: discussionMessage.isChannelPost, isForumPost: discussionMessage.isForumPost, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index ef8800d5da2..2793b3d89c0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -9,7 +9,6 @@ public enum SearchMessagesLocation: Equatable { case general(tags: MessageTags?, minDate: Int32?, maxDate: Int32?) case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, topMsgId: MessageId?, minDate: Int32?, maxDate: Int32?) - case publicForwards(messageId: MessageId) case sentMedia(tags: MessageTags?) } @@ -64,7 +63,7 @@ public struct SearchMessagesState: Equatable { fileprivate let additional: SearchMessagesPeerState? } -private func mergedState(transaction: Transaction, seedConfiguration: SeedConfiguration, state: SearchMessagesPeerState?, result: Api.messages.Messages?) -> SearchMessagesPeerState? { +private func mergedState(transaction: Transaction, seedConfiguration: SeedConfiguration, accountPeerId: PeerId, state: SearchMessagesPeerState?, result: Api.messages.Messages?) -> SearchMessagesPeerState? { guard let result = result else { return state } @@ -128,7 +127,7 @@ private func mergedState(transaction: Transaction, seedConfiguration: SeedConfig if let peerId = message.peerId, let peer = peers[peerId], peer.isForum { peerIsForum = true } - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var associatedThreadInfo: Message.AssociatedThreadInfo? if let threadId = message.threadId, let threadInfo = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId) { associatedThreadInfo = seedConfiguration.decodeMessageThreadInfo(threadInfo.data) @@ -354,46 +353,6 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation return .single((nil, nil)) } } - case let .publicForwards(messageId): - remoteSearchResult = account.postbox.transaction { transaction -> (Api.InputChannel?, Int32, MessageIndex?, Api.InputPeer, Int32?) in - let sourcePeer = transaction.getPeer(messageId.peerId) - let inputChannel = sourcePeer.flatMap { apiInputChannel($0) } - let statsDatacenterId = (transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData)?.statsDatacenterId - - var lowerBound: MessageIndex? - if let state = state, let message = state.main.messages.last { - lowerBound = message.index - } - if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { - return (inputChannel, state?.main.nextRate ?? 0, lowerBound, inputPeer, statsDatacenterId) - } else { - return (inputChannel, 0, lowerBound, .inputPeerEmpty, statsDatacenterId) - } - } - |> mapToSignal { (inputChannel, nextRate, lowerBound, inputPeer, statsDatacenterId) in - guard let inputChannel = inputChannel else { - return .complete() - } - - let request = Api.functions.stats.getMessagePublicForwards(channel: inputChannel, msgId: messageId.id, offsetRate: nextRate, offsetPeer: inputPeer, offsetId: lowerBound?.id.id ?? 0, limit: limit) - let signal: Signal - if let statsDatacenterId = statsDatacenterId, account.network.datacenterId != statsDatacenterId { - signal = account.network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil) - |> castError(MTRpcError.self) - |> mapToSignal { worker in - return worker.request(request) - } - } else { - signal = account.network.request(request, automaticFloodWait: false) - } - return signal - |> map { result -> (Api.messages.Messages?, Api.messages.Messages?) in - return (result, nil) - } - |> `catch` { _ -> Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> in - return .single((nil, nil)) - } - } case let .sentMedia(tags): let filter: Api.MessagesFilter = tags.flatMap { messageFilterForTagMask($0) } ?? .inputMessagesFilterEmpty @@ -413,7 +372,7 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation return remoteSearchResult |> mapToSignal { result, additionalResult -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> in return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in - var additional: SearchMessagesPeerState? = mergedState(transaction: transaction, seedConfiguration: account.postbox.seedConfiguration, state: state?.additional, result: additionalResult) + var additional: SearchMessagesPeerState? = mergedState(transaction: transaction, seedConfiguration: account.postbox.seedConfiguration, accountPeerId: account.peerId, state: state?.additional, result: additionalResult) if state?.additional == nil { switch location { @@ -454,13 +413,13 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation } } - let updatedState = SearchMessagesState(main: mergedState(transaction: transaction, seedConfiguration: account.postbox.seedConfiguration, state: state?.main, result: result) ?? SearchMessagesPeerState(messages: [], readStates: [:], threadInfo: [:], totalCount: 0, completed: true, nextRate: nil), additional: additional) + let updatedState = SearchMessagesState(main: mergedState(transaction: transaction, seedConfiguration: account.postbox.seedConfiguration, accountPeerId: account.peerId, state: state?.main, result: result) ?? SearchMessagesPeerState(messages: [], readStates: [:], threadInfo: [:], totalCount: 0, completed: true, nextRate: nil), additional: additional) return (mergedResult(updatedState), updatedState) } } } -func _internal_downloadMessage(postbox: Postbox, network: Network, messageId: MessageId) -> Signal { +func _internal_downloadMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, messageId: MessageId) -> Signal { return postbox.transaction { transaction -> Message? in return transaction.getMessage(messageId) } |> mapToSignal { message in @@ -529,7 +488,7 @@ func _internal_downloadMessage(postbox: Postbox, network: Network, messageId: Me var renderedMessages: [Message] = [] for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { renderedMessages.append(renderedMessage) } } @@ -607,7 +566,7 @@ func fetchRemoteMessage(accountPeerId: PeerId, postbox: Postbox, source: FetchMe if let peerId = message.peerId, let peer = transaction.getPeer(peerId) ?? parsedPeers.get(peerId), peer.isForum { peerIsForum = true } - if let message = StoreMessage(apiMessage: message, peerIsForum: peerIsForum, namespace: id.namespace), case let .Id(updatedId) = message.id { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: id.namespace), case let .Id(updatedId) = message.id { var addedExisting = false if transaction.getMessage(updatedId) != nil { transaction.updateMessage(updatedId, update: { _ in @@ -658,7 +617,7 @@ func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, thre messages = [] } for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { return message.index } } @@ -688,7 +647,7 @@ func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, thre messages = [] } for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: secondaryPeer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: secondaryPeer.isForum) { return message.index } } @@ -712,7 +671,7 @@ func _internal_searchMessageIdByTimestamp(account: Account, peerId: PeerId, thre messages = [] } for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: peer.isForum) { return message.index } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index b875cec9618..0c247175432 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -138,7 +138,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, for chat in chats { if let groupOrChannel = parsedPeers.get(chat.peerId) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 4467e1b48d2..0ddbc9f3515 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -835,7 +835,7 @@ public final class SparseMessageCalendar { let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) for message in messages { - if let parsedMessage = StoreMessage(apiMessage: message, peerIsForum: peer.isForum) { + if let parsedMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum) { parsedMessages.append(parsedMessage) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 1543dfb179e..22654b8f7d9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -244,6 +244,7 @@ public enum Stories { case timestamp case expirationTimestamp case media + case alternativeMedia case mediaAreas case text case entities @@ -266,6 +267,7 @@ public enum Stories { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: Media? + public let alternativeMedia: Media? public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -288,6 +290,7 @@ public enum Stories { timestamp: Int32, expirationTimestamp: Int32, media: Media?, + alternativeMedia: Media?, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], @@ -309,6 +312,7 @@ public enum Stories { self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media + self.alternativeMedia = alternativeMedia self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -339,6 +343,13 @@ public enum Stories { } else { self.media = nil } + + if let alternativeMediaData = try container.decodeIfPresent(Data.self, forKey: .alternativeMedia) { + self.alternativeMedia = PostboxDecoder(buffer: MemoryBuffer(data: alternativeMediaData)).decodeRootObject() as? Media + } else { + self.alternativeMedia = nil + } + self.mediaAreas = try container.decodeIfPresent([MediaArea].self, forKey: .mediaAreas) ?? [] self.text = try container.decode(String.self, forKey: .text) @@ -371,6 +382,14 @@ public enum Stories { let mediaData = encoder.makeData() try container.encode(mediaData, forKey: .media) } + + if let alternativeMedia = self.alternativeMedia { + let encoder = PostboxEncoder() + encoder.encodeRootObject(alternativeMedia) + let alternativeMediaData = encoder.makeData() + try container.encode(alternativeMediaData, forKey: .alternativeMedia) + } + try container.encode(self.mediaAreas, forKey: .mediaAreas) try container.encode(self.text, forKey: .text) @@ -410,6 +429,17 @@ public enum Stories { return false } } + + if let lhsAlternativeMedia = lhs.alternativeMedia, let rhsAlternativeMedia = rhs.alternativeMedia { + if !lhsAlternativeMedia.isEqual(to: rhsAlternativeMedia) { + return false + } + } else { + if (lhs.alternativeMedia == nil) != (rhs.alternativeMedia == nil) { + return false + } + } + if lhs.mediaAreas != rhs.mediaAreas { return false } @@ -1092,7 +1122,7 @@ func _internal_uploadStoryImpl( flags |= 1 << 4 } - let inputMediaAreas: [Api.MediaArea] = apiMediaAreasFromMediaAreas(mediaAreas) + let inputMediaAreas: [Api.MediaArea] = apiMediaAreasFromMediaAreas(mediaAreas, transaction: transaction) if !inputMediaAreas.isEmpty { flags |= 1 << 5 } @@ -1153,6 +1183,7 @@ func _internal_uploadStoryImpl( timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1281,7 +1312,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng flags |= 1 << 2 } - let inputMediaAreas: [Api.MediaArea]? = mediaAreas.flatMap(apiMediaAreasFromMediaAreas) + let inputMediaAreas: [Api.MediaArea]? = mediaAreas.flatMap { apiMediaAreasFromMediaAreas($0, transaction: transaction) } if let inputMediaAreas = inputMediaAreas, !inputMediaAreas.isEmpty { flags |= 1 << 3 } @@ -1334,6 +1365,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1364,6 +1396,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1557,6 +1590,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1586,6 +1620,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1774,11 +1809,22 @@ extension Stories.StoredItem { mergedForwardInfo = forwardFrom.flatMap(Stories.Item.ForwardInfo.init(apiForwardInfo:)) } + var parsedAlternativeMedia: Media? + switch media { + case let .messageMediaDocument(_, _, altDocument, _): + if let altDocument = altDocument { + parsedAlternativeMedia = telegramMediaFileFromApiDocument(altDocument) + } + default: + break + } + let item = Stories.Item( id: id, timestamp: date, expirationTimestamp: expireDate, media: parsedMedia, + alternativeMedia: parsedAlternativeMedia, mediaAreas: mediaAreas?.compactMap(mediaAreaFromApiMediaArea) ?? [], text: caption ?? "", entities: entities.flatMap { entities in return messageTextEntitiesFromApiEntities(entities) } ?? [], @@ -1842,6 +1888,7 @@ func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Ne timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2323,6 +2370,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2355,6 +2403,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 07ed8462882..6f9d330c653 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -60,6 +60,7 @@ public final class EngineStoryItem: Equatable { public let timestamp: Int32 public let expirationTimestamp: Int32 public let media: EngineMedia + public let alternativeMedia: EngineMedia? public let mediaAreas: [MediaArea] public let text: String public let entities: [MessageTextEntity] @@ -78,11 +79,12 @@ public final class EngineStoryItem: Equatable { public let myReaction: MessageReaction.Reaction? public let forwardInfo: ForwardInfo? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMedia: EngineMedia?, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp self.media = media + self.alternativeMedia = alternativeMedia self.mediaAreas = mediaAreas self.text = text self.entities = entities @@ -115,6 +117,9 @@ public final class EngineStoryItem: Equatable { if lhs.media != rhs.media { return false } + if lhs.alternativeMedia != rhs.alternativeMedia { + return false + } if lhs.mediaAreas != rhs.mediaAreas { return false } @@ -188,6 +193,7 @@ public extension EngineStoryItem { timestamp: self.timestamp, expirationTimestamp: self.expirationTimestamp, media: self.media._asMedia(), + alternativeMedia: self.alternativeMedia?._asMedia(), mediaAreas: self.mediaAreas, text: self.text, entities: self.entities, @@ -566,6 +572,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -710,6 +717,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -871,6 +879,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -918,6 +927,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -967,6 +977,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1012,6 +1023,7 @@ public final class PeerStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1181,6 +1193,7 @@ public final class PeerExpiringStoryListContext { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -1423,6 +1436,13 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun guard let inputPeer = inputPeer else { return .complete() } + + #if DEBUG + if "".isEmpty { + return .complete() + } + #endif + return network.request(Api.functions.stories.getPeerStories(peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index cde56283f50..ca0889f58b4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -18,6 +18,7 @@ public final class StoryPreloadInfo { public let peer: PeerReference public let storyId: Int32 public let media: EngineMedia + public let alternativeMedia: EngineMedia? public let reactions: [MessageReaction.Reaction] public let priority: Priority @@ -25,12 +26,14 @@ public final class StoryPreloadInfo { peer: PeerReference, storyId: Int32, media: EngineMedia, + alternativeMedia: EngineMedia?, reactions: [MessageReaction.Reaction], priority: Priority ) { self.peer = peer self.storyId = storyId self.media = media + self.alternativeMedia = alternativeMedia self.reactions = reactions self.priority = priority } @@ -77,7 +80,7 @@ public extension TelegramEngine { } public func downloadMessage(messageId: MessageId) -> Signal { - return _internal_downloadMessage(postbox: self.account.postbox, network: self.account.network, messageId: messageId) + return _internal_downloadMessage(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, messageId: messageId) } public func searchMessageIdByTimestamp(peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { @@ -173,7 +176,7 @@ public extension TelegramEngine { return _internal_markAllChatsAsRead(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager) } - public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal { + public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal { return _internal_getMessagesLoadIfNecessary(messageIds, postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, strategy: strategy) } @@ -651,6 +654,10 @@ public extension TelegramEngine { |> ignoreValues } + public func searchForumTopics(peerId: EnginePeer.Id, query: String) -> Signal<[EngineChatList.Item], NoError> { + return _internal_searchForumTopics(account: self.account, peerId: peerId, query: query) + } + public func debugAddHoles() -> Signal { return self.account.postbox.transaction { transaction -> Void in transaction.addHolesEverywhere(peerNamespaces: [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel], holeNamespace: Namespaces.Message.Cloud) @@ -1146,6 +1153,7 @@ public extension TelegramEngine { peer: peerReference, storyId: itemAndPeer.item.id, media: EngineMedia(media), + alternativeMedia: itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), reactions: reactions, priority: .top(position: nextPriority) ) @@ -1178,6 +1186,7 @@ public extension TelegramEngine { timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: item.media, + alternativeMedia: item.alternativeMedia, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 4731a335db8..49c7d41aa78 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -16,7 +16,7 @@ public enum AppStoreTransactionPurpose { case restore case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64) - case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) + case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) } private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal { @@ -59,7 +59,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount) } - case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount): + case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): return account.postbox.transaction { transaction -> Signal in guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return .complete() @@ -68,6 +68,9 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran if onlyNewSubscribers { flags |= (1 << 0) } + if showWinners { + flags |= (1 << 3) + } var additionalPeers: [Api.InputPeer] = [] if !additionalPeerIds.isEmpty { flags |= (1 << 1) @@ -80,7 +83,10 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran if !countries.isEmpty { flags |= (1 << 2) } - return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)) + if let _ = prizeDescription { + flags |= (1 << 4) + } + return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)) } |> switchToLatest } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 24bada7af35..f2b77784347 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -7,7 +7,8 @@ import TelegramApi public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) - case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) + case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) + case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) } @@ -214,7 +215,7 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa return .inputInvoiceMessage(peer: inputPeer, msgId: messageId.id) case let .slug(slug): return .inputInvoiceSlug(slug: slug) - case let .premiumGiveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount, option): + case let .premiumGiveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount, option): guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else { return nil } @@ -222,6 +223,9 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa if onlyNewSubscribers { flags |= (1 << 0) } + if showWinners { + flags |= (1 << 3) + } var additionalPeers: [Api.InputPeer] = [] if !additionalPeerIds.isEmpty { flags |= (1 << 1) @@ -234,7 +238,11 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa if !countries.isEmpty { flags |= (1 << 2) } - let input: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) + if let _ = prizeDescription { + flags |= (1 << 4) + } + + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) flags = 0 @@ -247,7 +255,34 @@ private func _internal_parseInputInvoice(transaction: Transaction, source: BotPa let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) - return .inputInvoicePremiumGiftCode(purpose: input, option: option) + return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) + case let .giftCode(users, currency, amount, option): + + + var inputUsers: [Api.InputUser] = [] + if !users.isEmpty { + for peerId in users { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputUser(peer) { + inputUsers.append(inputPeer) + } + } + } + + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount) + + + var flags: Int32 = 0 + if let _ = option.storeProductId { + flags |= (1 << 0) + } + if option.storeQuantity > 0 { + flags |= (1 << 1) + } + + let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) + + return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) + } } @@ -503,7 +538,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa account.stateManager.addUpdates(updates) var receiptMessageId: MessageId? for apiMessage in updates.messages { - if let message = StoreMessage(apiMessage: apiMessage, peerIsForum: false) { + if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { if let action = media as? TelegramMediaAction { if case .paymentSent = action.action { @@ -526,12 +561,14 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - case let .premiumGiveaway(_, _, _, _, randomId, _, _, _, _): + case let .premiumGiveaway(_, _, _, _, _, _, randomId, _, _, _, _): if message.globallyUniqueId == randomId { if case let .Id(id) = message.id { receiptMessageId = id } } + case .giftCode: + receiptMessageId = nil } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift index 784a06086eb..92af17aa86b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/GiftCodes.swift @@ -1,11 +1,12 @@ import Foundation +import Postbox import MtProtoKit import SwiftSignalKit import TelegramApi public struct PremiumGiftCodeInfo: Equatable { public let slug: String - public let fromPeerId: EnginePeer.Id + public let fromPeerId: EnginePeer.Id? public let messageId: EngineMessage.Id? public let toPeerId: EnginePeer.Id? public let date: Int32 @@ -138,13 +139,19 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m } } -func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> { - let flags: Int32 = 1 << 0 - return account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer in - guard let inputPeer = apiInputPeer(peer) else { - return .complete() +func _internal_premiumGiftCodeOptions(account: Account, peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> { + var flags: Int32 = 0 + if let _ = peerId { + flags |= 1 << 0 + } + return account.postbox.transaction { transaction -> Peer? in + if let peerId = peerId { + return transaction.getPeer(peerId) } + return nil + } + |> mapToSignal { peer in + let inputPeer = peer.flatMap(apiInputPeer) return account.network.request(Api.functions.payments.getPremiumGiftCodeOptions(flags: flags, boostPeer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal<[Api.PremiumGiftCodeOption]?, NoError> in @@ -184,11 +191,19 @@ func _internal_checkPremiumGiftCode(account: Account, slug: String) -> Signal Signal { return account.network.request(Api.functions.payments.applyGiftCode(slug: slug)) - |> mapError { _ -> ApplyPremiumGiftCodeError in + |> mapError { error -> ApplyPremiumGiftCodeError in + if error.errorDescription.hasPrefix("PREMIUM_SUB_ACTIVE_UNTIL_") { + if let range = error.errorDescription.range(of: "_", options: .backwards) { + if let value = Int32(error.errorDescription[range.upperBound...]) { + return .waitForExpiration(value) + } + } + } return .generic } |> mapToSignal { updates -> Signal in @@ -201,18 +216,19 @@ public enum LaunchPrepaidGiveawayError { case generic } -func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal { +func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) -> Signal { return account.postbox.transaction { transaction -> Signal in var flags: Int32 = 0 if onlyNewSubscribers { flags |= (1 << 0) } - + if showWinners { + flags |= (1 << 3) + } var inputPeer: Api.InputPeer? if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) { inputPeer = apiPeer } - var additionalPeers: [Api.InputPeer] = [] if !additionalPeerIds.isEmpty { flags |= (1 << 1) @@ -222,15 +238,16 @@ func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id } } } - if !countries.isEmpty { flags |= (1 << 2) } - + if let _ = prizeDescription { + flags |= (1 << 4) + } guard let inputPeer = inputPeer else { return .complete() } - return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: "", amount: 0))) + return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: "", amount: 0))) |> mapError { _ -> LaunchPrepaidGiveawayError in return .generic } @@ -257,8 +274,12 @@ extension PremiumGiftCodeInfo { switch apiCheckedGiftCode { case let .checkedGiftCode(flags, fromId, giveawayMsgId, toId, date, months, usedDate, _, _): self.slug = slug - self.fromPeerId = fromId.peerId - self.messageId = giveawayMsgId.flatMap { EngineMessage.Id(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: $0) } + self.fromPeerId = fromId?.peerId + if let fromId = fromId, let giveawayMsgId = giveawayMsgId { + self.messageId = EngineMessage.Id(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId) + } else { + self.messageId = nil + } self.toPeerId = toId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value($0)) } self.date = date self.months = months diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index d275bc2f265..0fe3e752cae 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -54,7 +54,7 @@ public extension TelegramEngine { return _internal_applyPremiumGiftCode(account: self.account, slug: slug) } - public func premiumGiftCodeOptions(peerId: EnginePeer.Id) -> Signal<[PremiumGiftCodeOption], NoError> { + public func premiumGiftCodeOptions(peerId: EnginePeer.Id?) -> Signal<[PremiumGiftCodeOption], NoError> { return _internal_premiumGiftCodeOptions(account: self.account, peerId: peerId) } @@ -62,8 +62,8 @@ public extension TelegramEngine { return _internal_getPremiumGiveawayInfo(account: self.account, peerId: peerId, messageId: messageId) } - public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal { - return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate) + public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) -> Signal { + return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index f0b53701721..de47654b99a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -235,17 +235,17 @@ public enum SendBotRequestedPeerError { case generic } -func _internal_sendBotRequestedPeer(account: Account, peerId: PeerId, messageId: MessageId, buttonId: Int32, requestedPeerId: PeerId) -> Signal { - let signal = account.postbox.transaction { transaction -> Signal in - - - if let peer = transaction.getPeer(peerId), let requestedPeer = transaction.getPeer(requestedPeerId) { - - let inputPeer = apiInputPeer(peer) - let inputRequestedPeer = apiInputPeer(requestedPeer) - - if let inputPeer = inputPeer, let inputRequestedPeer = inputRequestedPeer { - let signal = account.network.request(Api.functions.messages.sendBotRequestedPeer(peer: inputPeer, msgId: messageId.id, buttonId: buttonId, requestedPeer: inputRequestedPeer)) +func _internal_sendBotRequestedPeer(account: Account, peerId: PeerId, messageId: MessageId, buttonId: Int32, requestedPeerIds: [PeerId]) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId) { + var inputRequestedPeers: [Api.InputPeer] = [] + for requestedPeerId in requestedPeerIds { + if let requestedPeer = transaction.getPeer(requestedPeerId), let inputRequestedPeer = apiInputPeer(requestedPeer) { + inputRequestedPeers.append(inputRequestedPeer) + } + } + if let inputPeer = apiInputPeer(peer), !inputRequestedPeers.isEmpty { + let signal = account.network.request(Api.functions.messages.sendBotRequestedPeer(peer: inputPeer, msgId: messageId.id, buttonId: buttonId, requestedPeers: inputRequestedPeers)) |> mapError { error -> SendBotRequestedPeerError in return .generic } @@ -254,12 +254,9 @@ func _internal_sendBotRequestedPeer(account: Account, peerId: PeerId, messageId: } return signal } - } return .single(Void()) } |> castError(SendBotRequestedPeerError.self) - - return signal |> switchToLatest } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index be5115b6dc3..50b317f9467 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -595,7 +595,7 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { if let peer = transaction.getPeer(chat.peerId) { peers.append(peer) - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _) = chat, let participantsCount = participantsCount { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in var current = current as? CachedChannelData ?? CachedChannelData() var participantsSummary = current.participantsSummary diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 924d47c1790..718d9720e4d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -87,8 +87,10 @@ public enum AdminLogEventAction { case pinTopic(prevInfo: EngineMessageHistoryThread.Info?, newInfo: EngineMessageHistoryThread.Info?) case toggleForum(isForum: Bool) case toggleAntiSpam(isEnabled: Bool) - case changeNameColor(prev: PeerNameColor, new: PeerNameColor) - case changeBackgroundEmojiId(prev: Int64?, new: Int64?) + case changeNameColor(prevColor: PeerNameColor, prevIcon: Int64?, newColor: PeerNameColor, newIcon: Int64?) + case changeProfileColor(prevColor: PeerNameColor?, prevIcon: Int64?, newColor: PeerNameColor?, newIcon: Int64?) + case changeWallpaper(prev: TelegramWallpaper?, new: TelegramWallpaper?) + case changeStatus(prev: PeerEmojiStatus?, new: PeerEmojiStatus?) } public enum ChannelAdminLogEventError { @@ -202,16 +204,16 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case .messageEmpty: action = .updatePinned(nil) default: - if let message = StoreMessage(apiMessage: new, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { action = .updatePinned(rendered) } } case let .channelAdminLogEventActionEditMessage(prev, new): - if let prev = StoreMessage(apiMessage: prev, peerIsForum: peer.isForum), let prevRendered = locallyRenderedMessage(message: prev, peers: peers), let new = StoreMessage(apiMessage: new, peerIsForum: peer.isForum), let newRendered = locallyRenderedMessage(message: new, peers: peers) { + if let prev = StoreMessage(apiMessage: prev, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let prevRendered = locallyRenderedMessage(message: prev, peers: peers), let new = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let newRendered = locallyRenderedMessage(message: new, peers: peers) { action = .editMessage(prev: prevRendered, new: newRendered) } case let .channelAdminLogEventActionDeleteMessage(message): - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { action = .deleteMessage(rendered) } case .channelAdminLogEventActionParticipantJoin: @@ -245,7 +247,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case let .channelAdminLogEventActionDefaultBannedRights(prevBannedRights, newBannedRights): action = .updateDefaultBannedRights(prev: TelegramChatBannedRights(apiBannedRights: prevBannedRights), new: TelegramChatBannedRights(apiBannedRights: newBannedRights)) case let .channelAdminLogEventActionStopPoll(message): - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { action = .pollStopped(rendered) } case let .channelAdminLogEventActionChangeLinkedChat(prevValue, newValue): @@ -284,7 +286,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case let .channelAdminLogEventActionToggleNoForwards(new): action = .toggleCopyProtection(boolFromApiValue(new)) case let .channelAdminLogEventActionSendMessage(message): - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { action = .sendMessage(rendered) } case let .channelAdminLogEventActionChangeAvailableReactions(prevValue, newValue): @@ -348,10 +350,70 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net action = .toggleForum(isForum: newValue == .boolTrue) case let .channelAdminLogEventActionToggleAntiSpam(newValue): action = .toggleAntiSpam(isEnabled: newValue == .boolTrue) - case let .channelAdminLogEventActionChangeColor(prevValue, newValue): - action = .changeNameColor(prev: PeerNameColor(rawValue: prevValue), new: PeerNameColor(rawValue: newValue)) - case let .channelAdminLogEventActionChangeBackgroundEmoji(prevValue, newValue): - action = .changeBackgroundEmojiId(prev: prevValue, new: newValue) + case let .channelAdminLogEventActionChangePeerColor(prevValue, newValue): + var prevColorIndex: Int32 + var prevEmojiId: Int64? + switch prevValue { + case let .peerColor(_, color, backgroundEmojiIdValue): + prevColorIndex = color ?? 0 + prevEmojiId = backgroundEmojiIdValue + } + + var newColorIndex: Int32 + var newEmojiId: Int64? + switch newValue { + case let .peerColor(_, color, backgroundEmojiIdValue): + newColorIndex = color ?? 0 + newEmojiId = backgroundEmojiIdValue + } + + action = .changeNameColor(prevColor: PeerNameColor(rawValue: prevColorIndex), prevIcon: prevEmojiId, newColor: PeerNameColor(rawValue: newColorIndex), newIcon: newEmojiId) + case let .channelAdminLogEventActionChangeProfilePeerColor(prevValue, newValue): + var prevColorIndex: Int32? + var prevEmojiId: Int64? + switch prevValue { + case let .peerColor(_, color, backgroundEmojiIdValue): + prevColorIndex = color + prevEmojiId = backgroundEmojiIdValue + } + + var newColorIndex: Int32? + var newEmojiId: Int64? + switch newValue { + case let .peerColor(_, color, backgroundEmojiIdValue): + newColorIndex = color + newEmojiId = backgroundEmojiIdValue + } + + action = .changeProfileColor(prevColor: prevColorIndex.flatMap(PeerNameColor.init(rawValue:)), prevIcon: prevEmojiId, newColor: newColorIndex.flatMap(PeerNameColor.init(rawValue:)), newIcon: newEmojiId) + case let .channelAdminLogEventActionChangeWallpaper(prevValue, newValue): + let prev: TelegramWallpaper? + if case let .wallPaperNoFile(_, _, settings) = prevValue { + if settings == nil { + prev = nil + } else if case let .wallPaperSettings(flags, _, _, _, _, _, _, _) = settings, flags == 0 { + prev = nil + } else { + prev = TelegramWallpaper(apiWallpaper: prevValue) + } + } else { + prev = TelegramWallpaper(apiWallpaper: prevValue) + } + let new: TelegramWallpaper? + if case let .wallPaperNoFile(_, _, settings) = newValue { + if settings == nil { + new = nil + } else if case let .wallPaperSettings(flags, _, _, _, _, _, _, _) = settings, flags == 0 { + new = nil + } else { + new = TelegramWallpaper(apiWallpaper: newValue) + } + } else { + new = TelegramWallpaper(apiWallpaper: newValue) + } + action = .changeWallpaper(prev: prev, new: new) + case let .channelAdminLogEventActionChangeEmojiStatus(prevValue, newValue): + action = .changeStatus(prev: PeerEmojiStatus(apiStatus: prevValue), new: PeerEmojiStatus(apiStatus: newValue)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift index 2d726627b9a..b66146f16dc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift @@ -74,7 +74,7 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I for chat in chats { if let peer = transaction.getPeer(chat.peerId) { peers.append(EnginePeer(peer)) - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _) = chat, let participantsCount = participantsCount { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in var current = current as? CachedChannelData ?? CachedChannelData() var participantsSummary = current.participantsSummary diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 83a7ecae1a5..0788e8f6d9f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -812,7 +812,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForum { peerIsForum = true } - if let storeMessage = StoreMessage(apiMessage: message, peerIsForum: peerIsForum) { + if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) { var updatedStoreMessage = storeMessage if case let .Id(id) = storeMessage.id { if let channelPts = channelStates[id.peerId] { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 66ce170d5da..96c9eec56f2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -280,7 +280,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal S var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = [] for chat in chats { - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _) = chat { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { if let participantsCount = participantsCount { memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index 59d1a1b1738..9837949190f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -31,7 +31,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel] var participantsCounts: [PeerId: Int32] = [:] for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _, _, _, _, _): if let participantsCountValue = participantsCountValue { participantsCounts[chat.peerId] = participantsCountValue } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 8f6fca61b85..06cbf99f62e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -514,6 +514,10 @@ public extension EnginePeer { return self._asPeer().profileColor } + var emojiStatus: PeerEmojiStatus? { + return self._asPeer().emojiStatus + } + var backgroundEmojiId: Int64? { return self._asPeer().backgroundEmojiId } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift index 1bf83effde5..06eb921e8a2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RequestUserPhotos.swift @@ -23,7 +23,7 @@ public struct TelegramPeerPhoto { } } -func _internal_requestPeerPhotos(postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { +func _internal_requestPeerPhotos(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { return postbox.transaction{ transaction -> Peer? in return transaction.getPeer(peerId) } @@ -114,7 +114,7 @@ func _internal_requestPeerPhotos(postbox: Postbox, network: Network, peerId: Pee var renderedMessages: [Message] = [] for message in messages { - if let message = StoreMessage(apiMessage: message, peerIsForum: peer.isForum), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let renderedMessage = locallyRenderedMessage(message: message, peers: peers) { renderedMessages.append(renderedMessage) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index 6fa77daa0b0..ccdb358a1fa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -38,7 +38,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 64784073a61..1d684e538c5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -409,9 +409,8 @@ public extension TelegramEngine { return _internal_reportAntiSpamFalsePositive(account: self.account, peerId: peerId, messageId: messageId) } - public func requestPeerPhotos(peerId: PeerId) -> Signal<[TelegramPeerPhoto], NoError> { - return _internal_requestPeerPhotos(postbox: self.account.postbox, network: self.account.network, peerId: peerId) + return _internal_requestPeerPhotos(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId) } public func updateGroupSpecificStickerset(peerId: PeerId, info: StickerPackCollectionInfo?) -> Signal { @@ -505,8 +504,8 @@ public extension TelegramEngine { return _internal_addChannelMember(account: self.account, peerId: peerId, memberId: memberId) } - public func sendBotRequestedPeer(messageId: MessageId, buttonId: Int32, requestedPeerId: PeerId) -> Signal { - return _internal_sendBotRequestedPeer(account: self.account, peerId: messageId.peerId, messageId: messageId, buttonId: buttonId, requestedPeerId: requestedPeerId) + public func sendBotRequestedPeer(messageId: MessageId, buttonId: Int32, requestedPeerIds: [PeerId]) -> Signal { + return _internal_sendBotRequestedPeer(account: self.account, peerId: messageId.peerId, messageId: messageId, buttonId: buttonId, requestedPeerIds: requestedPeerIds) } public func addChannelMembers(peerId: PeerId, memberIds: [PeerId]) -> Signal { @@ -719,6 +718,18 @@ public extension TelegramEngine { return _internal_updatePeerNameColorAndEmoji(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) } + public func updatePeerNameColor(peerId: EnginePeer.Id, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { + return _internal_updatePeerNameColor(account: self.account, peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId) + } + + public func updatePeerProfileColor(peerId: EnginePeer.Id, profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?) -> Signal { + return _internal_updatePeerProfileColor(account: self.account, peerId: peerId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) + } + + public func updatePeerEmojiStatus(peerId: EnginePeer.Id, fileId: Int64?, expirationDate: Int32?) -> Signal { + return _internal_updatePeerEmojiStatus(account: self.account, peerId: peerId, fileId: fileId, expirationDate: expirationDate) + } + public func getChatListPeers(filterPredicate: ChatListFilterPredicate) -> Signal<[EnginePeer], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer] in return transaction.getChatListPeers(groupId: .root, filterPredicate: filterPredicate, additionalFilter: nil).map(EnginePeer.init) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 3cfeaffb16c..d09e8f7a921 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -431,14 +431,14 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch result { case let .chatFull(fullChat, chats, users): switch fullChat { - case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: notifySettings)]) case .chatFull: break } switch fullChat { - case let .channelFull(flags, flags2, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions, _): + case let .channelFull(flags, flags2, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, allowedReactions, _, wallpaper): var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) @@ -606,6 +606,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let membersHidden = (flags2 & (1 << 2)) != 0 let forumViewAsMessages = (flags2 & (1 << 6)) != 0 + let wallpaper = wallpaper.flatMap { TelegramWallpaper(apiWallpaper: $0) } + return previous.withUpdatedFlags(channelFlags) .withUpdatedAbout(about) .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) @@ -634,6 +636,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedAllowedReactions(.known(mappedAllowedReactions)) .withUpdatedMembersHidden(.known(PeerMembersHidden(value: membersHidden))) .withUpdatedViewForumAsMessages(.known(forumViewAsMessages)) + .withUpdatedWallpaper(wallpaper) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift index 69de7472240..c4ebfb1334a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdatePeerInfo.swift @@ -98,25 +98,65 @@ func _internal_updatePeerNameColorAndEmoji(account: Account, peerId: EnginePeer. return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { - let flags: Int32 = (1 << 0) - return account.network.request(Api.functions.channels.updateColor(flags: flags, channel: inputChannel, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId ?? 0)) - |> mapError { error -> UpdatePeerNameColorAndEmojiError in - if error.errorDescription.hasPrefix("BOOSTS_REQUIRED") { - return .channelBoostRequired + var flagsReplies: Int32 = (1 << 2) + if backgroundEmojiId != nil { + flagsReplies |= 1 << 0 + } + + var flagsProfile: Int32 = (1 << 1) + if profileBackgroundEmojiId != nil { + flagsProfile |= 1 << 0 + } + if profileColor != nil { + flagsProfile |= (1 << 2) + } + + return combineLatest( + account.network.request(Api.functions.channels.updateColor(flags: flagsReplies, channel: inputChannel, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("CHAT_NOT_MODIFIED") { + return .single(nil) + } else { + return .fail(error) + } + }, + account.network.request(Api.functions.channels.updateColor(flags: flagsProfile, channel: inputChannel, color: profileColor?.rawValue, backgroundEmojiId: profileBackgroundEmojiId)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("CHAT_NOT_MODIFIED") { + return .single(nil) + } else { + return .fail(error) } - return .generic } - |> mapToSignal { result -> Signal in - account.stateManager.addUpdates(result) - - return account.postbox.transaction { transaction -> Void in - if let apiChat = apiUpdatesGroups(result).first { - let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [apiChat], users: []) - updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) - } - } - |> mapError { _ -> UpdatePeerNameColorAndEmojiError in } + ) + |> mapError { error -> UpdatePeerNameColorAndEmojiError in + if error.errorDescription.hasPrefix("BOOSTS_REQUIRED") { + return .channelBoostRequired } + return .generic + } + |> mapToSignal { repliesResult, profileResult -> Signal in + if let repliesResult = repliesResult { + account.stateManager.addUpdates(repliesResult) + } + if let profileResult = profileResult { + account.stateManager.addUpdates(profileResult) + } + + return account.postbox.transaction { transaction -> Void in + if let repliesResult = repliesResult, let apiChat = apiUpdatesGroups(repliesResult).first { + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [apiChat], users: []) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + } + if let profileResult = profileResult, let apiChat = apiUpdatesGroups(profileResult).first { + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [apiChat], users: []) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + } + } + |> mapError { _ -> UpdatePeerNameColorAndEmojiError in } + } } else { return .fail(.generic) } @@ -127,3 +167,144 @@ func _internal_updatePeerNameColorAndEmoji(account: Account, peerId: EnginePeer. |> castError(UpdatePeerNameColorAndEmojiError.self) |> switchToLatest } + +func _internal_updatePeerNameColor(account: Account, peerId: EnginePeer.Id, nameColor: PeerNameColor, backgroundEmojiId: Int64?) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId) { + if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { + var flagsReplies: Int32 = (1 << 2) + if backgroundEmojiId != nil { + flagsReplies |= 1 << 0 + } + + return account.network.request(Api.functions.channels.updateColor(flags: flagsReplies, channel: inputChannel, color: nameColor.rawValue, backgroundEmojiId: backgroundEmojiId)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("CHAT_NOT_MODIFIED") { + return .single(nil) + } else { + return .fail(error) + } + } + |> mapError { error -> UpdatePeerNameColorAndEmojiError in + if error.errorDescription.hasPrefix("BOOSTS_REQUIRED") { + return .channelBoostRequired + } + return .generic + } + |> mapToSignal { repliesResult -> Signal in + if let repliesResult = repliesResult { + account.stateManager.addUpdates(repliesResult) + } + + return account.postbox.transaction { transaction -> Void in + if let repliesResult = repliesResult, let apiChat = apiUpdatesGroups(repliesResult).first { + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [apiChat], users: []) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + } + } + |> mapError { _ -> UpdatePeerNameColorAndEmojiError in } + } + } else { + return .fail(.generic) + } + } else { + return .fail(.generic) + } + } + |> castError(UpdatePeerNameColorAndEmojiError.self) + |> switchToLatest +} + +func _internal_updatePeerProfileColor(account: Account, peerId: EnginePeer.Id, profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId) { + if let peer = peer as? TelegramChannel, let inputChannel = apiInputChannel(peer) { + var flagsProfile: Int32 = (1 << 1) + if profileBackgroundEmojiId != nil { + flagsProfile |= 1 << 0 + } + if profileColor != nil { + flagsProfile |= (1 << 2) + } + + return account.network.request(Api.functions.channels.updateColor(flags: flagsProfile, channel: inputChannel, color: profileColor?.rawValue, backgroundEmojiId: profileBackgroundEmojiId)) + |> map(Optional.init) + |> `catch` { error -> Signal in + if error.errorDescription.hasPrefix("CHAT_NOT_MODIFIED") { + return .single(nil) + } else { + return .fail(error) + } + } + |> mapError { error -> UpdatePeerNameColorAndEmojiError in + if error.errorDescription.hasPrefix("BOOSTS_REQUIRED") { + return .channelBoostRequired + } + return .generic + } + |> mapToSignal { profileResult -> Signal in + if let profileResult = profileResult { + account.stateManager.addUpdates(profileResult) + } + + return account.postbox.transaction { transaction -> Void in + if let profileResult = profileResult, let apiChat = apiUpdatesGroups(profileResult).first { + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [apiChat], users: []) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) + } + } + |> mapError { _ -> UpdatePeerNameColorAndEmojiError in } + } + } else { + return .fail(.generic) + } + } else { + return .fail(.generic) + } + } + |> castError(UpdatePeerNameColorAndEmojiError.self) + |> switchToLatest +} + +public enum UpdatePeerEmojiStatusError { + case generic +} + +func _internal_updatePeerEmojiStatus(account: Account, peerId: PeerId, fileId: Int64?, expirationDate: Int32?) -> Signal { + return account.postbox.transaction { transaction -> Api.InputChannel? in + let updatedStatus = fileId.flatMap { + PeerEmojiStatus(fileId: $0, expirationDate: expirationDate) + } + if let peer = transaction.getPeer(peerId) as? TelegramChannel { + updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedEmojiStatus(updatedStatus)], update: { _, updated in updated }) + } + + return transaction.getPeer(peerId).flatMap(apiInputChannel) + } + |> castError(UpdatePeerEmojiStatusError.self) + |> mapToSignal { inputChannel -> Signal in + guard let inputChannel = inputChannel else { + return .fail(.generic) + } + let mappedStatus: Api.EmojiStatus + if let fileId = fileId { + if let expirationDate = expirationDate { + mappedStatus = .emojiStatusUntil(documentId: fileId, until: expirationDate) + } else { + mappedStatus = .emojiStatus(documentId: fileId) + } + } else { + mappedStatus = .emojiStatusEmpty + } + return account.network.request(Api.functions.channels.updateEmojiStatus(channel: inputChannel, emojiStatus: mappedStatus)) + |> ignoreValues + |> `catch` { error -> Signal in + if error.errorDescription == "CHAT_NOT_MODIFIED" { + return .complete() + } else { + return .fail(.generic) + } + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index 53420ef5057..24045c8c6a3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -166,7 +166,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal Signal { - return webpagePreviewWithProgress(account: account, urls: urls) +public func webpagePreview(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal { + return webpagePreviewWithProgress(account: account, urls: urls, webpageId: webpageId, forPeerId: forPeerId) |> mapToSignal { next -> Signal in if case let .result(result) = next { return .single(.result(result)) @@ -35,7 +43,7 @@ public func normalizedWebpagePreviewUrl(url: String) -> String { return url } -public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil) -> Signal { +public func webpagePreviewWithProgress(account: Account, urls: [String], webpageId: MediaId? = nil, forPeerId: PeerId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in if let webpageId = webpageId, let webpage = transaction.getMedia(webpageId) as? TelegramMediaWebpage, let url = webpage.content.url { var sourceUrl = url @@ -44,6 +52,108 @@ public func webpagePreviewWithProgress(account: Account, urls: [String], webpage } return .single(.result(WebpagePreviewResult.Result(webpage: webpage, sourceUrl: sourceUrl))) } else { + if #available(iOS 13.0, macOS 10.15, *) { + if let forPeerId, forPeerId.namespace == Namespaces.Peer.SecretChat, let sourceUrl = urls.first, let url = URL(string: sourceUrl) { + let localHosts: [String] = [ + "twitter.com", + "www.twitter.com", + "instagram.com", + "www.instagram.com", + "tiktok.com", + "www.tiktok.com" + ] + if let host = url.host?.lowercased(), localHosts.contains(host) { + return Signal { subscriber in + subscriber.putNext(.progress(0.0)) + + let metadataProvider = LPMetadataProvider() + metadataProvider.shouldFetchSubresources = true + metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in + if let metadata = metadata { + let completeWithImage: (Data?) -> Void = { imageData in + var image: TelegramMediaImage? + if let imageData, let parsedImage = UIImage(data: imageData) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + var webpageType: String? + if image != nil { + webpageType = "photo" + } + + let webpage = TelegramMediaWebpage( + webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), + content: .Loaded(TelegramMediaWebpageLoadedContent( + url: sourceUrl, + displayUrl: metadata.url?.absoluteString ?? sourceUrl, + hash: 0, + type: webpageType, + websiteName: nil, + title: metadata.title, + text: metadata.value(forKey: "_summary") as? String, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: true, + image: image, + file: nil, + story: nil, + attributes: [], + instantPage: nil + )) + ) + subscriber.putNext(.result(WebpagePreviewResult.Result( + webpage: webpage, + sourceUrl: sourceUrl + ))) + subscriber.putCompletion() + } + + if let imageProvider = metadata.imageProvider { + imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in + guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else { + completeWithImage(nil) + return + } + completeWithImage(imageData) + }) + } else { + completeWithImage(nil) + } + } else { + subscriber.putNext(.result(nil)) + subscriber.putCompletion() + } + }) + + return ActionDisposable { + metadataProvider.cancel() + } + } + } + } + } + return account.network.requestWithAdditionalInfo(Api.functions.messages.getWebPagePreview(flags: 0, message: urls.joined(separator: " "), entities: nil), info: .progress) |> `catch` { _ -> Signal, NoError> in return .single(.result(.messageMediaEmpty)) diff --git a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift index 884cae8f3a1..8cee0d4f087 100644 --- a/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift +++ b/submodules/TelegramPresentationData/Sources/ChatControllerBackgroundNode.swift @@ -34,7 +34,7 @@ public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper i } else { var succeed = true switch wallpaper { - case .builtin: + case .builtin, .emoticon: if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { backgroundImage = UIImage(contentsOfFile: filePath)?.precomposed() } @@ -114,7 +114,7 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me } switch wallpaper { - case .builtin: + case .builtin, .emoticon: if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") { return .single((UIImage(contentsOfFile: filePath)?.precomposed(), true)) |> afterNext { image in diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 002043cccfa..11f2f15c3b4 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -624,6 +624,8 @@ public func serviceColor(for wallpaper: (TelegramWallpaper, UIImage?)) -> UIColo } else { return UIColor(rgb: 0x000000, alpha: 0.3) } + case .emoticon: + return UIColor(rgb: 0x000000, alpha: 0.3) } } @@ -708,6 +710,8 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M serviceBackgroundColorForWallpaper = (wallpaper, color) } } + case .emoticon: + return .single(UIColor(rgb: 0x000000, alpha: 0.3)) } } } @@ -875,3 +879,29 @@ public extension PresentationData { return PresentationData(strings: strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji) } } + +public func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { + let name: String + switch reference { + case let .builtin(theme): + switch theme { + case .dayClassic: + name = strings.Appearance_ThemeCarouselClassic + case .day: + name = strings.Appearance_ThemeCarouselDay + case .night: + name = strings.Appearance_ThemeCarouselNewNight + case .nightAccent: + name = strings.Appearance_ThemeCarouselTintedNight + } + case let .local(theme): + name = theme.title + case let .cloud(theme): + if let emoticon = theme.theme.emoticon { + name = emoticon + } else { + name = theme.theme.title + } + } + return name +} diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 41990cf8ba4..b8527bc98cb 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -341,8 +341,8 @@ public final class PrincipalThemeEssentialGraphics { self.clockMediaMinImage = emptyImage self.clockFreeFrameImage = emptyImage self.clockFreeMinImage = emptyImage - self.dateAndStatusMediaBackground = emptyImage - self.dateAndStatusFreeBackground = emptyImage + self.dateAndStatusMediaBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.message.mediaDateAndStatusFillColor)! + self.dateAndStatusFreeBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: serviceColor.dateFillStatic)! let impressionCountImage = UIImage(bundleImageName: "Chat/Message/ImpressionCount")! self.incomingDateAndStatusImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.message.incoming.secondaryTextColor)! @@ -368,8 +368,8 @@ public final class PrincipalThemeEssentialGraphics { self.mediaSelfExpiringIcon = generateTintedImage(image: selfExpiringImage, color: .white)! self.freeSelfExpiringIcon = generateTintedImage(image: selfExpiringImage, color: serviceColor.primaryText)! - self.radialIndicatorFileIconIncoming = emptyImage - self.radialIndicatorFileIconOutgoing = emptyImage + self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! + self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)! } else { self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) self.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 74836d1d470..7e54418f9bb 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -43,6 +43,7 @@ public struct PresentationResourcesSettings { public static let deleteAccount = renderIcon(name: "Chat/Info/GroupRemovedIcon") public static let powerSaving = renderIcon(name: "Settings/Menu/PowerSaving") public static let stories = renderIcon(name: "Settings/Menu/Stories") + public static let premiumGift = renderIcon(name: "Settings/Menu/Gift") public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -70,7 +71,7 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) - + public static let passport = renderIcon(name: "Settings/Menu/Passport") public static let watch = renderIcon(name: "Settings/Menu/Watch") diff --git a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift index e077993a229..fd95382431e 100644 --- a/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift +++ b/submodules/TelegramPresentationData/Sources/WallpaperUtils.swift @@ -7,6 +7,8 @@ public extension TelegramWallpaper { switch self { case .image: return false + case .emoticon: + return false case let .file(file): if self.isPattern, file.settings.colors.count == 1 && (file.settings.colors[0] == 0xffffff || file.settings.colors[0] == 0xffffffff) { return true @@ -47,6 +49,15 @@ public extension TelegramWallpaper { } } + var isEmoticon: Bool { + switch self { + case .emoticon: + return true + default: + return false + } + } + var dimensions: CGSize? { if case let .file(file) = self { return file.file.dimensions?.cgSize diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 072dbe48e05..7248c286e59 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -625,8 +625,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(titleString._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) } } - case let .customText(text, entities): - attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()) + case let .customText(text, entities, additionalAttributes): + let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) + if let additionalAttributes { + for (range, key, value) in additionalAttributes.attributes { + mutableAttributedString.addAttribute(key, value: value, range: range) + } + } + attributedString = mutableAttributedString case let .botDomainAccessGranted(domain): attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor) case let .botAppAccessGranted(appName, type): @@ -733,7 +739,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case let .topicCreated(title, iconColor, iconFileId): if forForumOverview { let maybeFileId = iconFileId ?? 0 - attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicCreated(".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicCreated(".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } else { attributedString = NSAttributedString(string: strings.Notification_ForumTopicCreated, font: titleFont, textColor: primaryTextColor) } @@ -757,9 +763,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, maybeFileId = info.icon ?? 0 } if isHidden { - attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicHidden(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicHidden(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicUnhidden(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicUnhidden(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } } else { if isHidden { @@ -794,9 +800,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, maybeFileId = info.icon ?? 0 } if isClosed { - attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicClosed(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicClosed(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicReopened(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_OverviewTopicReopened(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } } else { if isClosed { @@ -834,7 +840,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicRenamedIconChangedAuthor(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".", title)._tuple, body: bodyAttributes, argumentAttributes: [ 0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), - 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)]) + 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)]) ]) } else { attributedString = NSAttributedString(string: strings.Notification_ForumTopicRenamed(title).string, font: titleFont, textColor: primaryTextColor) @@ -867,9 +873,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, title = info.title } if case let .user(user) = message.author { - attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChangedAuthor(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".")._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChangedAuthor(EnginePeer.user(user).displayTitle(strings: strings, displayOrder: nameDisplayOrder), ".")._tuple, body: bodyAttributes, argumentAttributes: [0: peerMentionAttributes(primaryTextColor: primaryTextColor, peerId: user.id), 1: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } else { - attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChanged(".")._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, topicInfo: maybeFileId == 0 ? (message.threadId ?? 0, EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) + attributedString = addAttributesToStringWithRanges(strings.Notification_ForumTopicIconChanged(".")._tuple, body: bodyAttributes, argumentAttributes: [0: MarkdownAttributeSet(font: titleFont, textColor: primaryTextColor, additionalAttributes: [ChatTextInputAttributes.customEmoji.rawValue: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: maybeFileId, file: nil, custom: maybeFileId == 0 ? .topic(id: message.threadId ?? 0, info: EngineMessageHistoryThread.Info(title: title, icon: nil, iconColor: iconColor)) : nil)])]) } } case let .suggestedProfilePhoto(image): @@ -880,10 +886,19 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } case .attachMenuBotAllowed: attributedString = NSAttributedString(string: strings.Notification_BotWriteAllowed, font: titleFont, textColor: primaryTextColor) - case let .requestedPeer(_, peerId): + case let .requestedPeer(_, peerIds): let botName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "" - let peerName = message.peers[peerId].flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "" - attributedString = addAttributesToStringWithRanges(strings.Notification_RequestedPeer(peerName, botName)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId), (1, message.id.peerId)])) + var attributePeerIds: [(Int, EnginePeer.Id?)] = [] + let resultTitleString: PresentationStrings.FormattedString + if peerIds.count == 1 { + attributePeerIds.append((0, peerIds.first)) + attributePeerIds.append((1, message.id.peerId)) + resultTitleString = strings.Notification_RequestedPeer(peerDisplayTitles(peerIds, message.peers, strings: strings, nameDisplayOrder: nameDisplayOrder), botName) + } else { + attributePeerIds.append((1, message.id.peerId)) + resultTitleString = strings.Notification_RequestedPeerMultiple(peerDisplayTitles(peerIds, message.peers, strings: strings, nameDisplayOrder: nameDisplayOrder), botName) + } + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds)) case let .setChatWallpaper(_, forBoth): if message.author?.id == accountPeerId { if forBoth { @@ -894,8 +909,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = NSAttributedString(string: strings.Notification_YouChangedWallpaper, font: titleFont, textColor: primaryTextColor) } } else { - let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + if message.id.peerId.isGroupOrChannel { + attributedString = NSAttributedString(string: strings.Notification_ChannelChangedWallpaper, font: titleFont, textColor: primaryTextColor) + } else { + let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } } case .setSameChatWallpaper: if message.author?.id == accountPeerId { @@ -904,8 +923,19 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } - case .giftCode: - attributedString = NSAttributedString(string: strings.Notification_GiftLink, font: titleFont, textColor: primaryTextColor) + case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _): + if boostPeerId == nil, let currency, let amount { + let price = formatCurrencyAmount(amount, currency: currency) + if message.author?.id == accountPeerId { + attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]) + attributes[1] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else { + attributedString = NSAttributedString(string: strings.Notification_GiftLink, font: titleFont, textColor: primaryTextColor) + } case .giveawayLaunched: let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index c6d56b6baad..b8ff54a41fb 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -20,7 +20,6 @@ NGDEPS = [ "//Nicegram/NGModels:NGModels", "@FirebaseSDK//:FirebaseCrashlytics", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatChatBanner", - "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPremium", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAssistantUI", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAuth", @@ -446,6 +445,9 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode", "//submodules/TelegramUI/Components/Chat/ChatQrCodeScreen", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen", + "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", + "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD index 6d202c1f728..ac35b7c0f23 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/BUILD +++ b/submodules/TelegramUI/Components/Calls/CallScreen/BUILD @@ -67,6 +67,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/AppBundle", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/Contents.json b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/Contents.json new file mode 100644 index 00000000000..41da0c21f9b --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "locksettings (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/locksettings (1).pdf b/submodules/TelegramUI/Components/Calls/CallScreen/CallScreenAssets.xcassets/Call/EmojiTooltipLock.imageset/locksettings (1).pdf new file mode 100644 index 0000000000000000000000000000000000000000..e1ec6d0145baee6a2ba09c9b79721f98b7641af8 GIT binary patch literal 2505 zcma)8!EV$r5WV{=e5uqPn#4{V+fr4DZYe^5C|hn7hmcL%McD*4TU7XZ#vA*^+w1`! zR^AuS%$sM=*eAEwH|HYJj0v9c{`(Kc`Najlykt$ca(|hO@$_TUJ?@@(PTJ z+OYJx`Ms{2<-04sxQqYQ1N+59lI;i2!sHyY`8Ry3Hw^RnWT?C8bDo$nLS#}TR%Rko zQ&g1{CKE~%B89d_bCuYPCW6=GtH`X*3tlGD6#3px3sqW0h_ab1OHO2+PfLh4G|9e&k3Y3AhtPGTa=|DL`JmrXk87Y?&Qxhm78xtr;Iom)P z$^e0SK%jBWBFrb&P8ib1Do6wl5VB@qnk8OtWP>fyVVkgVHG?uZ8c+`iGzL;L>;dNZ5;(J+TMYu`mByX5@c7ijvNI zmRmadQOzI#Ca%niaC}J>Ys<{?O^i`RQ8n-)QOXES{UXuhDivY@amT(Wgpnp*d}>zf>Z*U?_tSldBA+@n+Cy-(Y=-`MTQ>uD4}_)nw&=TE z(@{Y8ItbuxwIz(xg}aBMH-v-mwn#s$SHzUN1eQF+S@h3jD`xZuLLyJ)fhU*WfPhop zi)vT3{o|qO%eHx78P{xVfbN%<68&{fyJw!Vh{8974h>%Et|?5>`_~JAkB6 zuYE}=Cttg5wQid&bDQ`6n&dbg`b~4jPuHunQIvGq_dD(q-V2=Gt^YQ}e*fsIw!3{g NQktBdoV@*f^ABKT|Aqhn literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal index d2bc62de18e..1402203f290 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal @@ -236,7 +236,7 @@ fragment half4 callBlobFragment( return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha); } -kernel void videoYUVToRGBA( +kernel void videoBiPlanarToRGBA( texture2d inTextureY [[ texture(0) ]], texture2d inTextureUV [[ texture(1) ]], texture2d outTexture [[ texture(2) ]], @@ -249,6 +249,22 @@ kernel void videoYUVToRGBA( outTexture.write(color, threadPosition); } +kernel void videoTriPlanarToRGBA( + texture2d inTextureY [[ texture(0) ]], + texture2d inTextureU [[ texture(1) ]], + texture2d inTextureV [[ texture(2) ]], + texture2d outTexture [[ texture(3) ]], + uint2 threadPosition [[ thread_position_in_grid ]] +) { + half y = inTextureY.read(threadPosition).r; + uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2); + half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r); + half2 uv = inUV - half2(0.5, 0.5); + + half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0); + outTexture.write(color, threadPosition); +} + vertex QuadVertexOut mainVideoVertex( const device Rectangle &rect [[ buffer(0) ]], const device uint2 &mirror [[ buffer(1) ]], diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index c1a20242ebf..de46ab41e9e 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -38,28 +38,34 @@ final class ButtonGroupView: OverlayMaskContainerView { } let content: Content + let isEnabled: Bool let action: () -> Void - init(content: Content, action: @escaping () -> Void) { + init(content: Content, isEnabled: Bool, action: @escaping () -> Void) { self.content = content + self.isEnabled = isEnabled self.action = action } } final class Notice { let id: AnyHashable + let icon: String let text: String - init(id: AnyHashable, text: String) { + init(id: AnyHashable, icon: String, text: String) { self.id = id + self.icon = icon self.text = text } } private var buttons: [Button]? private var buttonViews: [Button.Content.Key: ContentOverlayButton] = [:] - private var noticeViews: [AnyHashable: NoticeView] = [:] + private var closeButtonView: CloseButtonView? + + var closePressed: (() -> Void)? override init(frame: CGRect) { super.init(frame: frame) @@ -79,7 +85,7 @@ final class ButtonGroupView: OverlayMaskContainerView { return result } - func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { + func update(size: CGSize, insets: UIEdgeInsets, minWidth: CGFloat, controlsHidden: Bool, displayClose: Bool, buttons: [Button], notices: [Notice], transition: Transition) -> CGFloat { self.buttons = buttons let buttonSize: CGFloat = 56.0 @@ -122,7 +128,7 @@ final class ButtonGroupView: OverlayMaskContainerView { noticesHeight += buttonNoticeSpacing } } - let noticeSize = noticeView.update(text: notice.text, constrainedWidth: size.width - insets.left * 2.0 - 16.0 * 2.0, transition: noticeTransition) + let noticeSize = noticeView.update(icon: notice.icon, text: notice.text, constrainedWidth: size.width - insets.left * 2.0 - 16.0 * 2.0, transition: noticeTransition) let noticeFrame = CGRect(origin: CGPoint(x: floor((size.width - noticeSize.width) * 0.5), y: nextNoticeY - noticeSize.height), size: noticeSize) noticesHeight += noticeSize.height nextNoticeY -= noticeSize.height + noticeSpacing @@ -163,6 +169,46 @@ final class ButtonGroupView: OverlayMaskContainerView { } var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5) + if displayClose { + let closeButtonView: CloseButtonView + var closeButtonTransition = transition + var animateIn = false + if let current = self.closeButtonView { + closeButtonView = current + } else { + closeButtonTransition = closeButtonTransition.withAnimation(.none) + animateIn = true + + closeButtonView = CloseButtonView() + self.closeButtonView = closeButtonView + self.addSubview(closeButtonView) + closeButtonView.pressAction = { [weak self] in + guard let self else { + return + } + self.closePressed?() + } + } + let closeButtonSize = CGSize(width: minWidth, height: buttonSize) + closeButtonView.update(text: "Close", size: closeButtonSize, transition: closeButtonTransition) + closeButtonTransition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((size.width - closeButtonSize.width) * 0.5), y: buttonY), size: closeButtonSize)) + + if animateIn && !transition.animation.isImmediate { + closeButtonView.animateIn() + } + } else { + if let closeButtonView = self.closeButtonView { + self.closeButtonView = nil + if !transition.animation.isImmediate { + closeButtonView.animateOut(completion: { [weak closeButtonView] in + closeButtonView?.removeFromSuperview() + }) + } else { + closeButtonView.removeFromSuperview() + } + } + } + for button in buttons { let title: String let image: UIImage? @@ -213,11 +259,12 @@ final class ButtonGroupView: OverlayMaskContainerView { Transition.immediate.setScale(view: buttonView, scale: 0.001) buttonView.alpha = 0.0 transition.setScale(view: buttonView, scale: 1.0) - transition.setAlpha(view: buttonView, alpha: 1.0) } + transition.setAlpha(view: buttonView, alpha: displayClose ? 0.0 : 1.0) + buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))) - buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition) + buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, isEnabled: button.isEnabled, title: title, transition: buttonTransition) buttonX += buttonSize + buttonSpacing } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift new file mode 100644 index 00000000000..110814ead64 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CloseButtonView.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import UIKitRuntimeUtils + +final class CloseButtonView: HighlightTrackingButton, OverlayMaskContainerViewProtocol { + private struct Params: Equatable { + var text: String + var size: CGSize + + init(text: String, size: CGSize) { + self.text = text + self.size = size + } + } + + private let backdropBackgroundView: RoundedCornersView + private let backgroundView: RoundedCornersView + private let backgroundMaskView: UIView + private let backgroundClippingView: UIView + + private let duration: Double = 5.0 + private var fillTime: Double = 0.0 + + private let backgroundTextView: TextView + private let backgroundTextClippingView: UIView + + private let textView: TextView + + var pressAction: (() -> Void)? + + private var params: Params? + private var updateDisplayLink: SharedDisplayLinkDriver.Link? + + let maskContents: UIView + override static var layerClass: AnyClass { + return MirroringLayer.self + } + + override init(frame: CGRect) { + self.backdropBackgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backdropBackgroundView.update(cornerRadius: 12.0, transition: .immediate) + + self.backgroundView = RoundedCornersView(color: .white, smoothCorners: true) + self.backgroundView.update(cornerRadius: 12.0, transition: .immediate) + self.backgroundView.isUserInteractionEnabled = false + + self.backgroundMaskView = UIView() + self.backgroundMaskView.backgroundColor = .white + self.backgroundView.mask = self.backgroundMaskView + if let filter = makeLuminanceToAlphaFilter() { + self.backgroundMaskView.layer.filters = [filter] + } + + self.backgroundClippingView = UIView() + self.backgroundClippingView.clipsToBounds = true + self.backgroundClippingView.layer.cornerRadius = 12.0 + + self.backgroundTextClippingView = UIView() + self.backgroundTextClippingView.clipsToBounds = true + + self.backgroundTextView = TextView() + self.textView = TextView() + + self.maskContents = UIView() + + self.maskContents.addSubview(self.backdropBackgroundView) + + super.init(frame: frame) + + (self.layer as? MirroringLayer)?.targetLayer = self.maskContents.layer + + self.backgroundTextClippingView.addSubview(self.backgroundTextView) + self.backgroundTextClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundTextClippingView) + + self.backgroundClippingView.addSubview(self.backgroundView) + self.backgroundClippingView.isUserInteractionEnabled = false + self.addSubview(self.backgroundClippingView) + + self.backgroundMaskView.addSubview(self.textView) + + self.internalHighligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "sublayerTransform") + let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } else { + let t = self.layer.presentation()?.transform ?? layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in + guard let self, completed else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + (self.layer as? MirroringLayer)?.didEnterHierarchy = { [weak self] in + guard let self else { + return + } + if self.fillTime < self.duration && self.updateDisplayLink == nil { + self.updateDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.fillTime = min(self.duration, self.fillTime + deltaTime) + if let params = self.params { + self.update(params: params, transition: .immediate) + } + + if self.fillTime >= self.duration { + self.updateDisplayLink = nil + } + }) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.pressAction?() + } + + func animateIn() { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func update(text: String, size: CGSize, transition: Transition) { + let params = Params(text: text, size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: Transition) { + let fillFraction: CGFloat = CGFloat(self.fillTime / self.duration) + + let sideInset: CGFloat = 12.0 + let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .black, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + let _ = self.backgroundTextView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.semibold.rawValue, color: .white, constrainedWidth: params.size.width - sideInset * 2.0, transition: .immediate) + + transition.setFrame(view: self.backdropBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: params.size)) + + let progressWidth: CGFloat = max(0.0, min(params.size.width, floorToScreenPixels(fillFraction * params.size.width))) + let backgroundClippingFrame = CGRect(origin: CGPoint(x: progressWidth, y: 0.0), size: CGSize(width: params.size.width - progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundClippingView, position: backgroundClippingFrame.center) + transition.setBounds(view: self.backgroundClippingView, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX, y: 0.0), size: backgroundClippingFrame.size)) + + let backgroundTextClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: params.size.height)) + transition.setPosition(view: self.backgroundTextClippingView, position: backgroundTextClippingFrame.center) + transition.setBounds(view: self.backgroundTextClippingView, bounds: CGRect(origin: CGPoint(), size: backgroundTextClippingFrame.size)) + + let textFrame = CGRect(origin: CGPoint(x: floor((params.size.width - textSize.width) * 0.5), y: floor((params.size.height - textSize.height) * 0.5)), size: textSize) + transition.setFrame(view: self.textView, frame: textFrame) + transition.setFrame(view: self.backgroundTextView, frame: textFrame) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index 12d62e51045..ac1ea32db8b 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -9,12 +9,14 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV var image: UIImage? var isSelected: Bool var isDestructive: Bool + var isEnabled: Bool - init(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool) { + init(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool) { self.size = size self.image = image self.isSelected = isSelected self.isDestructive = isDestructive + self.isEnabled = isEnabled } } @@ -63,8 +65,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width if highlighted { - self.layer.removeAnimation(forKey: "opacity") - self.layer.removeAnimation(forKey: "transform") + self.layer.removeAnimation(forKey: "sublayerTransform") let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut)) transition.setScale(layer: self.layer, scale: topScale) } else { @@ -94,13 +95,15 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV self.action?() } - func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, title: String, transition: Transition) { - let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive) + func update(size: CGSize, image: UIImage?, isSelected: Bool, isDestructive: Bool, isEnabled: Bool, title: String, transition: Transition) { + let contentParams = ContentParams(size: size, image: image, isSelected: isSelected, isDestructive: isDestructive, isEnabled: isEnabled) if self.contentParams != contentParams { self.contentParams = contentParams self.updateContent(contentParams: contentParams, transition: transition) } + self.isUserInteractionEnabled = isEnabled + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) let textSize = self.textView.update(string: title, fontSize: 13.0, fontWeight: 0.0, color: .white, constrainedWidth: 100.0, transition: .immediate) @@ -129,7 +132,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV context.clip(to: imageFrame, mask: cgImage) context.setBlendMode(contentParams.isSelected ? .copy : .normal) - context.setFillColor(contentParams.isSelected ? UIColor.clear.cgColor : UIColor(white: 1.0, alpha: 1.0).cgColor) + context.setFillColor(contentParams.isSelected ? UIColor(white: 1.0, alpha: contentParams.isEnabled ? 0.0 : 0.5).cgColor : UIColor(white: 1.0, alpha: contentParams.isEnabled ? 1.0 : 0.5).cgColor) context.fill(imageFrame) context.resetClip() @@ -137,12 +140,8 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV } }) - if !transition.animation.isImmediate, let currentContentViewIsSelected = self.currentContentViewIsSelected, currentContentViewIsSelected != contentParams.isSelected, let previousImage = self.contentView.image, let image { + if !transition.animation.isImmediate, let currentContentViewIsSelected = self.currentContentViewIsSelected, currentContentViewIsSelected != contentParams.isSelected, let previousImage = self.contentView.image { self.contentView.layer.mask = nil - let _ = previousImage - let _ = image - let _ = currentContentViewIsSelected - let previousContentView = UIImageView(image: previousImage) previousContentView.frame = self.contentView.frame self.addSubview(previousContentView) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift index 3a0cf981bd2..f2b0ffd7ec1 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiExpandedInfoView.swift @@ -5,10 +5,10 @@ import ComponentFlow final class EmojiExpandedInfoView: OverlayMaskContainerView { private struct Params: Equatable { - var constrainedWidth: CGFloat + var width: CGFloat - init(constrainedWidth: CGFloat) { - self.constrainedWidth = constrainedWidth + init(width: CGFloat) { + self.width = width } } @@ -129,8 +129,8 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { return nil } - func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize { - let params = Params(constrainedWidth: constrainedWidth) + func update(width: CGFloat, transition: Transition) -> CGSize { + let params = Params(width: width) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.size } @@ -142,16 +142,12 @@ final class EmojiExpandedInfoView: OverlayMaskContainerView { private func update(params: Params, transition: Transition) -> CGSize { let buttonHeight: CGFloat = 56.0 - var constrainedWidth = params.constrainedWidth - constrainedWidth = min(constrainedWidth, 300.0) + let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) + let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: params.width - 16.0 * 2.0, transition: transition) - let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition) - - let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0 let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight - let size = CGSize(width: contentWidth, height: contentHeight) + let size = CGSize(width: params.width, height: contentHeight) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift index 05afa7b2dcd..efb0bdd5d6c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/EmojiTooltipView.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import AppBundle private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloat) { context.saveGState() @@ -41,6 +42,7 @@ final class EmojiTooltipView: OverlayMaskContainerView { private let text: String private let backgroundView: UIImageView + private let iconView: UIImageView private let textView: TextView private var currentLayout: Layout? @@ -50,18 +52,18 @@ final class EmojiTooltipView: OverlayMaskContainerView { self.backgroundView = UIImageView() + self.iconView = UIImageView() self.textView = TextView() super.init(frame: CGRect()) self.maskContents.addSubview(self.backgroundView) + self.addSubview(self.iconView) self.addSubview(self.textView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") - - } func animateIn() { @@ -92,9 +94,16 @@ final class EmojiTooltipView: OverlayMaskContainerView { } private func update(params: Params) -> CGSize { - let horizontalInset: CGFloat = 12.0 + let horizontalInset: CGFloat = 13.0 let verticalInset: CGFloat = 10.0 let arrowHeight: CGFloat = 8.0 + let iconSpacing: CGFloat = 5.0 + + if self.iconView.image == nil { + self.iconView.image = UIImage(bundleImageName: "Call/EmojiTooltipLock")?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = .white + } + let iconSize = self.iconView.image?.size ?? CGSize(width: 12.0, height: 12.0) let textSize = self.textView.update( string: self.text, @@ -105,9 +114,11 @@ final class EmojiTooltipView: OverlayMaskContainerView { transition: .immediate ) - let size = CGSize(width: textSize.width + horizontalInset * 2.0, height: arrowHeight + textSize.height + verticalInset * 2.0) + let size = CGSize(width: iconSize.width + iconSpacing + textSize.width + horizontalInset * 2.0, height: arrowHeight + textSize.height + verticalInset * 2.0) + + self.iconView.frame = CGRect(origin: CGPoint(x: horizontalInset, y: arrowHeight + verticalInset + floorToScreenPixels((textSize.height - iconSize.height) * 0.5)), size: iconSize) - self.textView.frame = CGRect(origin: CGPoint(x: horizontalInset, y: arrowHeight + verticalInset), size: textSize) + self.textView.frame = CGRect(origin: CGPoint(x: horizontalInset + iconSize.width + iconSpacing, y: arrowHeight + verticalInset), size: textSize) self.backgroundView.image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift index 80331e8ad4a..d307040facf 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/NoticeView.swift @@ -2,16 +2,19 @@ import Foundation import UIKit import Display import ComponentFlow +import AppBundle final class NoticeView: OverlayMaskContainerView { private let backgroundView: RoundedCornersView private let textContainer: UIView + private let iconView: UIImageView private let textView: TextView override init(frame: CGRect) { self.backgroundView = RoundedCornersView(color: .white) self.textContainer = UIView() self.textContainer.clipsToBounds = true + self.iconView = UIImageView() self.textView = TextView() super.init(frame: frame) @@ -20,6 +23,7 @@ final class NoticeView: OverlayMaskContainerView { self.maskContents.addSubview(self.backgroundView) + self.textContainer.addSubview(self.iconView) self.textContainer.addSubview(self.textView) self.addSubview(self.textContainer) } @@ -38,7 +42,9 @@ final class NoticeView: OverlayMaskContainerView { self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) self.backgroundView.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.backgroundView.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) - self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + self.iconView.layer.animatePosition(from: CGPoint(x: -4.0, y: 0.0), to: CGPoint(), duration: 0.15, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + //self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) self.textContainer.layer.cornerRadius = self.bounds.height * 0.5 self.textContainer.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.textContainer.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in guard let self, completed else { @@ -55,18 +61,28 @@ final class NoticeView: OverlayMaskContainerView { self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } - func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + func update(icon: String, text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { let sideInset: CGFloat = 12.0 let verticalInset: CGFloat = 6.0 + let iconSpacing: CGFloat = -3.0 + + if self.iconView.image == nil { + self.iconView.image = UIImage(bundleImageName: icon)?.withRenderingMode(.alwaysTemplate) + self.iconView.tintColor = .white + } + let iconSize = self.iconView.image?.size ?? CGSize(width: 12.0, height: 12.0) let textSize = self.textView.update(string: text, fontSize: 15.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedWidth - sideInset * 2.0, transition: .immediate) - let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) + let size = CGSize(width: iconSize.width + iconSpacing + textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundView.update(cornerRadius: floor(size.height * 0.5), transition: transition) transition.setFrame(view: self.textContainer, frame: CGRect(origin: CGPoint(), size: size)) - transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)) + + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - iconSize.height) * 0.5) + 4.0, y: verticalInset + floorToScreenPixels((textSize.height - iconSize.height) * 0.5)), size: iconSize)) + + transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset + iconSize.width + iconSpacing, y: verticalInset), size: textSize)) return size } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift new file mode 100644 index 00000000000..b5a52fd67fd --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallPictureInPictureView.swift @@ -0,0 +1,248 @@ +import Foundation +import AVKit +import AVFoundation +import CoreMedia +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit + +private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { + var maybeFormat: CMVideoFormatDescription? + let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat) + if status != noErr { + return nil + } + guard let format = maybeFormat else { + return nil + } + + var timingInfo = CMSampleTimingInfo( + duration: CMTimeMake(value: 1, timescale: 30), + presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), + decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) + ) + + var maybeSampleBuffer: CMSampleBuffer? + let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer) + + if (bufferStatus != noErr) { + return nil + } + guard let sampleBuffer = maybeSampleBuffer else { + return nil + } + + let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray + let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary + dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber + + return sampleBuffer +} + +final class PrivateCallPictureInPictureView: UIView { + private final class SampleBufferView: UIView { + override static var layerClass: AnyClass { + return AVSampleBufferDisplayLayer.self + } + } + + private final class AnimationTrackingLayer: SimpleLayer { + var onAnimation: ((CAAnimation) -> Void)? + + override func add(_ anim: CAAnimation, forKey key: String?) { + super.add(anim, forKey: key) + + if key == "bounds" { + self.onAnimation?(anim) + } + } + } + + private final class AnimationTrackingView: UIView { + override static var layerClass: AnyClass { + return AnimationTrackingLayer.self + } + + var onAnimation: ((CAAnimation) -> Void)? { + didSet { + (self.layer as? AnimationTrackingLayer)?.onAnimation = self.onAnimation + } + } + } + + private let animationTrackingView: AnimationTrackingView + + private let videoContainerView: UIView + private let sampleBufferView: SampleBufferView + + private var videoMetrics: VideoContainerView.VideoMetrics? + private var videoDisposable: Disposable? + + var isRenderingEnabled: Bool = false { + didSet { + if self.isRenderingEnabled != oldValue { + self.updateContents() + } + } + } + var video: VideoSource? { + didSet { + if self.video !== oldValue { + self.videoDisposable?.dispose() + if let video = self.video { + self.videoDisposable = video.addOnUpdated({ [weak self] in + guard let self else { + return + } + if self.isRenderingEnabled { + self.updateContents() + } + }) + } + } + } + } + + override static var layerClass: AnyClass { + return AVSampleBufferDisplayLayer.self + } + + override init(frame: CGRect) { + self.animationTrackingView = AnimationTrackingView() + + self.videoContainerView = UIView() + self.sampleBufferView = SampleBufferView() + + super.init(frame: frame) + + self.addSubview(self.animationTrackingView) + + self.backgroundColor = .black + + self.videoContainerView.addSubview(self.sampleBufferView) + self.addSubview(self.videoContainerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateContents() { + guard let video = self.video, let currentOutput = video.currentOutput else { + return + } + guard let pixelBuffer = currentOutput.dataBuffer.pixelBuffer else { + return + } + let videoMetrics = VideoContainerView.VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) + if self.videoMetrics != videoMetrics { + self.videoMetrics = videoMetrics + self.setNeedsLayout() + } + + if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: pixelBuffer) { + (self.sampleBufferView.layer as? AVSampleBufferDisplayLayer)?.enqueue(sampleBuffer) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + let size = self.bounds.size + if size.width.isZero || size.height.isZero { + return + } + + var animationTemplate: CAAnimation? + self.animationTrackingView.onAnimation = { animation in + animationTemplate = animation + } + self.animationTrackingView.frame = CGRect(origin: CGPoint(), size: size) + self.animationTrackingView.onAnimation = nil + + let _ = animationTemplate + + let animationDuration = CATransaction.animationDuration() + let timingFunction = CATransaction.animationTimingFunction() + + let mappedTransition: Transition + if self.sampleBufferView.bounds.isEmpty { + mappedTransition = .immediate + } else if animationDuration > 0.0 && !CATransaction.disableActions() { + let mappedCurve: Transition.Animation.Curve + if let timingFunction { + var controlPoint0: [Float] = [0.0, 0.0] + var controlPoint1: [Float] = [0.0, 0.0] + timingFunction.getControlPoint(at: 1, values: &controlPoint0) + timingFunction.getControlPoint(at: 2, values: &controlPoint1) + mappedCurve = .custom(controlPoint0[0], controlPoint0[1], controlPoint1[0], controlPoint1[1]) + } else if animationDuration >= 0.5 { + mappedCurve = .spring + } else { + mappedCurve = .easeInOut + } + mappedTransition = Transition(animation: .curve( + duration: animationDuration, + curve: mappedCurve + )) + } else { + mappedTransition = .immediate + } + + if let videoMetrics = self.videoMetrics { + let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation) + + var rotatedResolution = videoMetrics.resolution + var videoIsRotated = false + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { + rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) + videoIsRotated = true + } + + var videoSize = rotatedResolution.aspectFitted(size) + let boundingAspectRatio = size.width / size.height + let videoAspectRatio = videoSize.width / videoSize.height + let isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15 + if isFillingBounds { + videoSize = rotatedResolution.aspectFilled(size) + } + + let rotatedBoundingSize = videoIsRotated ? CGSize(width: size.height, height: size.width) : size + let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize + + let videoFrame = rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5)) + + let apply: () -> Void = { + self.videoContainerView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + self.videoContainerView.bounds = CGRect(origin: CGPoint(), size: rotatedBoundingSize) + self.videoContainerView.transform = CGAffineTransformMakeRotation(CGFloat(resolvedRotationAngle)) + + self.sampleBufferView.center = videoFrame.center + self.sampleBufferView.bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + + if let sublayers = self.sampleBufferView.layer.sublayers { + if sublayers.count > 1, !sublayers[0].bounds.isEmpty { + sublayers[0].position = CGPoint(x: videoFrame.width * 0.5, y: videoFrame.height * 0.5) + sublayers[0].bounds = CGRect(origin: CGPoint(), size: videoFrame.size) + } + } + } + + if !mappedTransition.animation.isImmediate { + apply() + } else { + UIView.performWithoutAnimation { + apply() + } + } + } + } +} + +@available(iOS 15.0, *) +final class PrivateCallPictureInPictureController: AVPictureInPictureVideoCallViewController { + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift index 53ada011856..ee5b9998909 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift @@ -11,7 +11,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { let blurredLayer: MetalEngineSubjectLayer final class BlurState: ComputeState { - let computePipelineStateYUVToRGBA: MTLComputePipelineState + let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState + let computePipelineStateYUVTriPlanarToRGBA: MTLComputePipelineState let computePipelineStateHorizontal: MTLComputePipelineState let computePipelineStateVertical: MTLComputePipelineState let downscaleKernel: MPSImageBilinearScale @@ -20,13 +21,22 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { guard let library = metalLibrary(device: device) else { return nil } - guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else { + + guard let functionVideoBiPlanarToRGBA = library.makeFunction(name: "videoBiPlanarToRGBA") else { return nil } - guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else { + guard let computePipelineStateYUVBiPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoBiPlanarToRGBA) else { return nil } - self.computePipelineStateYUVToRGBA = computePipelineStateYUVToRGBA + self.computePipelineStateYUVBiPlanarToRGBA = computePipelineStateYUVBiPlanarToRGBA + + guard let functionVideoTriPlanarToRGBA = library.makeFunction(name: "videoTriPlanarToRGBA") else { + return nil + } + guard let computePipelineStateYUVTriPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoTriPlanarToRGBA) else { + return nil + } + self.computePipelineStateYUVTriPlanarToRGBA = computePipelineStateYUVTriPlanarToRGBA guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else { return nil @@ -107,7 +117,7 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { return } - let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized) + let rgbaTextureSpec = TextureSpec(width: Int(videoTextures.resolution.width), height: Int(videoTextures.resolution.height), pixelFormat: .rgba8UnsignedNormalized) if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec { self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec) } @@ -136,10 +146,19 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1) let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1) - computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVToRGBA) - computeEncoder.setTexture(videoTextures.y, index: 0) - computeEncoder.setTexture(videoTextures.uv, index: 1) - computeEncoder.setTexture(rgbaTexture, index: 2) + switch videoTextures.textureLayout { + case let .biPlanar(biPlanar): + computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA) + computeEncoder.setTexture(biPlanar.y, index: 0) + computeEncoder.setTexture(biPlanar.uv, index: 1) + computeEncoder.setTexture(rgbaTexture, index: 2) + case let .triPlanar(triPlanar): + computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA) + computeEncoder.setTexture(triPlanar.y, index: 0) + computeEncoder.setTexture(triPlanar.u, index: 1) + computeEncoder.setTexture(triPlanar.u, index: 2) + computeEncoder.setTexture(rgbaTexture, index: 3) + } computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.endEncoding() @@ -198,8 +217,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject { encoder.setFragmentTexture(blurredTexture, index: 0) - var brightness: Float = 1.0 - var saturation: Float = 1.2 + var brightness: Float = 0.7 + var saturation: Float = 1.3 var overlay: SIMD4 = SIMD4(1.0, 1.0, 1.0, 0.2) encoder.setFragmentBytes(&brightness, length: 4, index: 0) encoder.setFragmentBytes(&saturation, length: 4, index: 1) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift new file mode 100644 index 00000000000..6b549343134 --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RatingView.swift @@ -0,0 +1,73 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +final class RatingView: OverlayMaskContainerView { + private let backgroundView: RoundedCornersView + private let textContainer: UIView + private let textView: TextView + + override init(frame: CGRect) { + self.backgroundView = RoundedCornersView(color: .white) + self.textContainer = UIView() + self.textContainer.clipsToBounds = true + self.textView = TextView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.maskContents.addSubview(self.backgroundView) + + self.textContainer.addSubview(self.textView) + self.addSubview(self.textContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateIn() { + let delay: Double = 0.2 + + self.layer.animateScale(from: 0.001, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) + + self.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + self.backgroundView.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.backgroundView.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + + self.textContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay) + self.textContainer.layer.cornerRadius = self.bounds.height * 0.5 + self.textContainer.layer.animateFrame(from: CGRect(origin: CGPoint(x: (self.bounds.width - self.bounds.height) * 0.5, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height)), to: self.textContainer.frame, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in + guard let self, completed else { + return + } + self.textContainer.layer.cornerRadius = 0.0 + }) + } + + func animateOut(completion: @escaping () -> Void) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + } + + func update(text: String, constrainedWidth: CGFloat, transition: Transition) -> CGSize { + let sideInset: CGFloat = 12.0 + let verticalInset: CGFloat = 6.0 + + let textSize = self.textView.update(string: text, fontSize: 15.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedWidth - sideInset * 2.0, transition: .immediate) + let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(cornerRadius: floor(size.height * 0.5), transition: transition) + + transition.setFrame(view: self.textContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.textView, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: textSize)) + + return size + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift index 664616101a6..af64a8b1a0c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/RoundedCornersView.swift @@ -5,17 +5,16 @@ import ComponentFlow final class RoundedCornersView: UIImageView { private let color: UIColor + private let smoothCorners: Bool + private var currentCornerRadius: CGFloat? private var cornerImage: UIImage? - init(color: UIColor) { + init(color: UIColor, smoothCorners: Bool = false) { self.color = color + self.smoothCorners = smoothCorners super.init(image: nil) - - if #available(iOS 13.0, *) { - self.layer.cornerCurve = .circular - } } required init?(coder: NSCoder) { @@ -26,10 +25,33 @@ final class RoundedCornersView: UIImageView { guard let cornerRadius = self.currentCornerRadius else { return } - if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 { + if cornerRadius == 0.0 { + if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 { + } else { + self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(self.color.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5) + } } else { - let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) - self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + if self.smoothCorners { + let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath) + context.setFillColor(self.color.cgColor) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5) + } + } else { + let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0) + if let cornerImage = self.cornerImage, cornerImage.size == size { + } else { + self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color) + } + } } self.image = self.cornerImage self.clipsToBounds = false @@ -52,6 +74,14 @@ final class RoundedCornersView: UIImageView { if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil { self.layer.cornerRadius = previousCornerRadius } + if #available(iOS 13.0, *) { + if self.smoothCorners { + self.layer.cornerCurve = .continuous + } else { + self.layer.cornerCurve = .circular + } + + } transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift index 2c798396274..f2949a4f91b 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/StatusView.swift @@ -166,7 +166,8 @@ final class StatusView: UIView { enum WaitingState { case requesting case ringing - case generatingKeys + case connecting + case reconnecting } struct ActiveState: Equatable { @@ -299,8 +300,10 @@ final class StatusView: UIView { textString = "Requesting" case .ringing: textString = "Ringing" - case .generatingKeys: - textString = "Exchanging encryption keys" + case .connecting: + textString = "Connecting" + case .reconnecting: + textString = "Reconnecting" } case let .active(activeState): monospacedDigits = true @@ -310,7 +313,11 @@ final class StatusView: UIView { textString = stringForDuration(Int(duration)) signalStrength = activeState.signalStrength case let .terminated(terminatedState): - textString = stringForDuration(Int(terminatedState.duration)) + if Int(terminatedState.duration) == 0 { + textString = " " + } else { + textString = stringForDuration(Int(terminatedState.duration)) + } } var contentSize = CGSize() diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift index 6d321a3a374..4a55077b858 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -9,6 +9,26 @@ private let shadowImage: UIImage? = { UIImage(named: "Call/VideoGradient")?.precomposed() }() +func resolveVideoRotationAngle(angle: Float, followsDeviceOrientation: Bool, interfaceOrientation: UIInterfaceOrientation) -> Float { + if !followsDeviceOrientation { + return angle + } + let interfaceAngle: Float + switch interfaceOrientation { + case .portrait, .unknown: + interfaceAngle = 0.0 + case .landscapeLeft: + interfaceAngle = Float.pi * 0.5 + case .landscapeRight: + interfaceAngle = Float.pi * 3.0 / 2.0 + case .portraitUpsideDown: + interfaceAngle = Float.pi + @unknown default: + interfaceAngle = 0.0 + } + return (angle + interfaceAngle).truncatingRemainder(dividingBy: Float.pi * 2.0) +} + private final class VideoContainerLayer: SimpleLayer { let contentsLayer: SimpleLayer @@ -44,14 +64,16 @@ final class VideoContainerView: HighlightTrackingButton { private struct Params: Equatable { var size: CGSize var insets: UIEdgeInsets + var interfaceOrientation: UIInterfaceOrientation var cornerRadius: CGFloat var controlsHidden: Bool var isMinimized: Bool var isAnimatedOut: Bool - init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) { + init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) { self.size = size self.insets = insets + self.interfaceOrientation = interfaceOrientation self.cornerRadius = cornerRadius self.controlsHidden = controlsHidden self.isMinimized = isMinimized @@ -59,14 +81,16 @@ final class VideoContainerView: HighlightTrackingButton { } } - private struct VideoMetrics: Equatable { + struct VideoMetrics: Equatable { var resolution: CGSize var rotationAngle: Float + var followsDeviceOrientation: Bool var sourceId: Int - init(resolution: CGSize, rotationAngle: Float, sourceId: Int) { + init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool, sourceId: Int) { self.resolution = resolution self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation self.sourceId = sourceId } } @@ -74,10 +98,12 @@ final class VideoContainerView: HighlightTrackingButton { private final class FlipAnimationInfo { let isForward: Bool let previousRotationAngle: Float + let followsDeviceOrientation: Bool - init(isForward: Bool, previousRotationAngle: Float) { + init(isForward: Bool, previousRotationAngle: Float, followsDeviceOrientation: Bool) { self.isForward = isForward self.previousRotationAngle = previousRotationAngle + self.followsDeviceOrientation = followsDeviceOrientation } } @@ -141,11 +167,11 @@ final class VideoContainerView: HighlightTrackingButton { var videoMetrics: VideoMetrics? if let currentOutput = self.video?.currentOutput { if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId { - self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle)) + self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle, followsDeviceOrientation: previousVideo.followsDeviceOrientation)) } self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) } else { self.videoLayer.video = nil } @@ -164,7 +190,7 @@ final class VideoContainerView: HighlightTrackingButton { var videoMetrics: VideoMetrics? if let currentOutput = self.video?.currentOutput { self.videoLayer.video = currentOutput - videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId) + videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId) } else { self.videoLayer.video = nil } @@ -382,7 +408,7 @@ final class VideoContainerView: HighlightTrackingButton { self.dragPositionAnimatorLink = nil return } - let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false) + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: false) let targetPosition = videoLayout.rotatedVideoFrame.center self.dragVelocity = self.updateVelocityUsingSpring( @@ -443,8 +469,8 @@ final class VideoContainerView: HighlightTrackingButton { self.update(previousParams: params, params: params, transition: transition) } - func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { - let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) + func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) { + let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut) if self.params == params { return } @@ -462,6 +488,7 @@ final class VideoContainerView: HighlightTrackingButton { private struct MinimizedLayout { var videoIsRotated: Bool + var videoSize: CGSize var rotatedVideoSize: CGSize var rotatedVideoResolution: CGSize var rotatedVideoFrame: CGRect @@ -469,10 +496,10 @@ final class VideoContainerView: HighlightTrackingButton { var effectiveVideoFrame: CGRect } - private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout { + private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, resolvedRotationAngle: Float, applyDragPosition: Bool) -> MinimizedLayout { var rotatedResolution = videoMetrics.resolution var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) videoIsRotated = true } @@ -505,13 +532,14 @@ final class VideoContainerView: HighlightTrackingButton { var videoTransform = CATransform3DIdentity videoTransform.m34 = 1.0 / 600.0 - videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0) + videoTransform = CATransform3DRotate(videoTransform, CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0) if params.isAnimatedOut { videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0) } return MinimizedLayout( videoIsRotated: videoIsRotated, + videoSize: videoSize, rotatedVideoSize: rotatedVideoSize, rotatedVideoResolution: rotatedVideoResolution, rotatedVideoFrame: rotatedVideoFrame, @@ -530,25 +558,27 @@ final class VideoContainerView: HighlightTrackingButton { } self.appliedVideoMetrics = videoMetrics + let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation) + if params.isMinimized { self.isFillingBounds = false - let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true) + let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolvedRotationAngle, applyDragPosition: true) - transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center) + transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.effectiveVideoFrame.center) self.videoContainerLayer.contentsLayer.masksToBounds = true if self.disappearingVideoLayer != nil { self.videoContainerLayer.contentsLayer.backgroundColor = UIColor.black.cgColor } - transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize), completion: { [weak self] completed in + transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.effectiveVideoFrame.size), completion: { [weak self] completed in guard let self, completed else { return } self.videoContainerLayer.contentsLayer.masksToBounds = false self.videoContainerLayer.contentsLayer.backgroundColor = nil }) - self.videoContainerLayer.update(size: videoLayout.rotatedVideoSize, transition: transition) + self.videoContainerLayer.update(size: videoLayout.effectiveVideoFrame.size, transition: transition) var videoTransition = transition if self.videoLayer.bounds.isEmpty { @@ -558,8 +588,8 @@ final class VideoContainerView: HighlightTrackingButton { if let disappearingVideoLayer = self.disappearingVideoLayer { self.disappearingVideoLayer = nil - let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true) - let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize + let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: disappearingVideoLayer.videoMetrics.rotationAngle, followsDeviceOrientation: disappearingVideoLayer.videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: true) + let initialDisappearingVideoSize = disappearingVideoLayout.effectiveVideoFrame.size if !disappearingVideoLayer.isAlphaAnimationInitiated { disappearingVideoLayer.isAlphaAnimationInitiated = true @@ -568,19 +598,9 @@ final class VideoContainerView: HighlightTrackingButton { var videoTransform = self.videoContainerLayer.transform var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0) let previousVideoScale: CGPoint - if flipAnimationInfo.previousRotationAngle == Float.pi * 0.5 { - axis.x = -1.0 - previousVideoScale = CGPoint(x: 1.0, y: -1.0) - } else if flipAnimationInfo.previousRotationAngle == Float.pi { - axis.y = -1.0 - previousVideoScale = CGPoint(x: -1.0, y: -1.0) - } else if flipAnimationInfo.previousRotationAngle == Float.pi * 3.0 / 2.0 { - axis.x = 1.0 - previousVideoScale = CGPoint(x: 1.0, y: 1.0) - } else { - axis.y = 1.0 - previousVideoScale = CGPoint(x: -1.0, y: 1.0) - } + + axis.y = 1.0 + previousVideoScale = CGPoint(x: -1.0, y: 1.0) videoTransform = CATransform3DRotate(videoTransform, (flipAnimationInfo.isForward ? 1.0 : -1.0) * CGFloat.pi * 0.9999, axis.x, axis.y, axis.z) self.videoContainerLayer.transform = videoTransform @@ -588,7 +608,7 @@ final class VideoContainerView: HighlightTrackingButton { disappearingVideoLayer.videoLayer.zPosition = 1.0 transition.setZPosition(layer: disappearingVideoLayer.videoLayer, zPosition: -1.0) - disappearingVideoLayer.videoLayer.transform = CATransform3DMakeScale(previousVideoScale.x, previousVideoScale.y, 1.0) + disappearingVideoLayer.videoLayer.transform = CATransform3DConcat(disappearingVideoLayout.videoTransform, CATransform3DMakeScale(previousVideoScale.x, previousVideoScale.y, 1.0)) animateFlipDisappearingVideo = disappearingVideoLayer disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer() @@ -610,26 +630,41 @@ final class VideoContainerView: HighlightTrackingButton { self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + let mappedDisappearingSize: CGSize + if videoLayout.videoIsRotated { + mappedDisappearingSize = CGSize(width: initialDisappearingVideoSize.height, height: initialDisappearingVideoSize.width) + } else { + mappedDisappearingSize = initialDisappearingVideoSize + } + self.videoLayer.position = disappearingVideoLayer.videoLayer.position - self.videoLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize)) + self.videoLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(mappedDisappearingSize)) self.videoLayer.blurredLayer.position = disappearingVideoLayer.videoLayer.blurredLayer.position - self.videoLayer.blurredLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize)) + self.videoLayer.blurredLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(mappedDisappearingSize)) } - let disappearingVideoSize = initialDisapparingVideoSize.aspectFilled(videoLayout.rotatedVideoSize) - transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5)) + let disappearingFitVideoSize: CGSize + if disappearingVideoLayout.videoIsRotated { + disappearingFitVideoSize = CGSize(width: videoLayout.effectiveVideoFrame.size.height, height: videoLayout.effectiveVideoFrame.size.width) + } else { + disappearingFitVideoSize = videoLayout.effectiveVideoFrame.size + } + + let disappearingVideoSize = disappearingVideoLayout.rotatedVideoSize.aspectFilled(disappearingFitVideoSize) + transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: CGPoint(x: videoLayout.effectiveVideoFrame.width * 0.5, y: videoLayout.effectiveVideoFrame.height * 0.5)) transition.setBounds(layer: disappearingVideoLayer.videoLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize)) transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center) transition.setBounds(layer: disappearingVideoLayer.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize)) } let animateFlipDisappearingVideoLayer = animateFlipDisappearingVideo?.videoLayer - transition.setTransform(layer: self.videoContainerLayer, transform: videoLayout.videoTransform, completion: { [weak animateFlipDisappearingVideoLayer] _ in + transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DIdentity, completion: { [weak animateFlipDisappearingVideoLayer] _ in animateFlipDisappearingVideoLayer?.removeFromSuperlayer() }) - transition.setPosition(layer: self.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5)) + transition.setPosition(layer: self.videoLayer, position: CGPoint(x: videoLayout.videoSize.width * 0.5, y: videoLayout.videoSize.height * 0.5)) transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize)) + videoTransition.setTransform(layer: self.videoLayer, transform: videoLayout.videoTransform) transition.setPosition(layer: self.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center) transition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 0.0) @@ -652,7 +687,7 @@ final class VideoContainerView: HighlightTrackingButton { } else { var rotatedResolution = videoMetrics.resolution var videoIsRotated = false - if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 { + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) videoIsRotated = true } @@ -674,7 +709,6 @@ final class VideoContainerView: HighlightTrackingButton { let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0)) let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution - let rotatedBoundingSize = videoIsRotated ? CGSize(width: params.size.height, height: params.size.width) : params.size let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize let rotatedVideoBoundingSize = params.size let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedVideoBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedVideoBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize) @@ -698,8 +732,8 @@ final class VideoContainerView: HighlightTrackingButton { }) transition.setPosition(layer: self.videoContainerLayer, position: CGPoint(x: params.size.width * 0.5, y: params.size.height * 0.5)) - transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBoundingSize)) - self.videoContainerLayer.update(size: rotatedBoundingSize, transition: transition) + transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: params.size)) + self.videoContainerLayer.update(size: params.size, transition: transition) var videoTransition = transition if self.videoLayer.bounds.isEmpty { @@ -710,12 +744,20 @@ final class VideoContainerView: HighlightTrackingButton { } } + let videoFrame = rotatedVideoSize.centered(around: CGPoint(x: params.size.width * 0.5, y: params.size.height * 0.5)) + if let disappearingVideoLayer = self.disappearingVideoLayer { self.disappearingVideoLayer = nil if !disappearingVideoLayer.isAlphaAnimationInitiated { disappearingVideoLayer.isAlphaAnimationInitiated = true + self.videoLayer.position = disappearingVideoLayer.videoLayer.position + self.videoLayer.blurredLayer.position = disappearingVideoLayer.videoLayer.blurredLayer.position + + transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: videoFrame.center) + transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoFrame.center) + let alphaTransition: Transition = .easeInOut(duration: 0.2) let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerValue] _ in @@ -728,13 +770,14 @@ final class VideoContainerView: HighlightTrackingButton { } } - transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + transition.setPosition(layer: self.videoLayer, position: videoFrame.center) + videoTransition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: videoFrame.size)) + videoTransition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0)) - videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5))) - videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) + transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center) videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size)) videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0) - videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)) + videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0)) if !params.isAnimatedOut { self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift index 68977a2a5c6..a0805cbe019 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift @@ -16,18 +16,67 @@ public final class VideoSourceOutput { public static let vertical = MirrorDirection(rawValue: 1 << 1) } + open class DataBuffer { + open var pixelBuffer: CVPixelBuffer? { + return nil + } + + public init() { + } + } + + public final class BiPlanarTextureLayout { + public let y: MTLTexture + public let uv: MTLTexture + + public init(y: MTLTexture, uv: MTLTexture) { + self.y = y + self.uv = uv + } + } + + public final class TriPlanarTextureLayout { + public let y: MTLTexture + public let u: MTLTexture + public let v: MTLTexture + + public init(y: MTLTexture, u: MTLTexture, v: MTLTexture) { + self.y = y + self.u = u + self.v = v + } + } + + public enum TextureLayout { + case biPlanar(BiPlanarTextureLayout) + case triPlanar(TriPlanarTextureLayout) + } + + public final class NativeDataBuffer: DataBuffer { + private let pixelBufferValue: CVPixelBuffer + override public var pixelBuffer: CVPixelBuffer? { + return self.pixelBufferValue + } + + public init(pixelBuffer: CVPixelBuffer) { + self.pixelBufferValue = pixelBuffer + } + } + public let resolution: CGSize - public let y: MTLTexture - public let uv: MTLTexture + public let textureLayout: TextureLayout + public let dataBuffer: DataBuffer public let rotationAngle: Float + public let followsDeviceOrientation: Bool public let mirrorDirection: MirrorDirection public let sourceId: Int - public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, mirrorDirection: MirrorDirection, sourceId: Int) { + public init(resolution: CGSize, textureLayout: TextureLayout, dataBuffer: DataBuffer, rotationAngle: Float, followsDeviceOrientation: Bool, mirrorDirection: MirrorDirection, sourceId: Int) { self.resolution = resolution - self.y = y - self.uv = uv + self.textureLayout = textureLayout + self.dataBuffer = dataBuffer self.rotationAngle = rotationAngle + self.followsDeviceOrientation = followsDeviceOrientation self.mirrorDirection = mirrorDirection self.sourceId = sourceId } @@ -161,7 +210,18 @@ public final class FileVideoSource: VideoSource { resolution.width = floor(resolution.width * self.sizeMultiplicator.x) resolution.height = floor(resolution.height * self.sizeMultiplicator.y) - self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, mirrorDirection: [], sourceId: self.sourceId) + self.currentOutput = Output( + resolution: resolution, + textureLayout: .biPlanar(Output.BiPlanarTextureLayout( + y: yTexture, + uv: uvTexture + )), + dataBuffer: Output.NativeDataBuffer(pixelBuffer: buffer), + rotationAngle: rotationAngle, + followsDeviceOrientation: false, + mirrorDirection: [], + sourceId: self.sourceId + ) return true } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index b67bebc28c1..db793d3e58c 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -1,58 +1,14 @@ import Foundation +import AVFoundation +import AVKit import UIKit import Display import MetalEngine import ComponentFlow import SwiftSignalKit +import UIKitRuntimeUtils -/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject { - final class RenderState: RenderToLayerState { - let pipelineState: MTLRenderPipelineState - - required init?(device: MTLDevice) { - guard let library = metalLibrary(device: device) else { - return nil - } - guard let vertexFunction = library.makeFunction(name: "edgeTestVertex"), let fragmentFunction = library.makeFunction(name: "edgeTestFragment") else { - return nil - } - - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.vertexFunction = vertexFunction - pipelineDescriptor.fragmentFunction = fragmentFunction - pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one - - guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { - return nil - } - self.pipelineState = pipelineState - } - } - - var internalData: MetalEngineSubjectInternalData? - - func update(context: MetalEngineSubjectContext) { - context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: 300, height: 300), edgeInset: 100), state: RenderState.self, layer: self, commands: { encoder, placement in - let effectiveRect = placement.effectiveRect - - var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width * 0.5), Float(effectiveRect.height)) - encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) - - var color = SIMD4(1.0, 0.0, 0.0, 1.0) - encoder.setFragmentBytes(&color, length: 4 * 4, index: 0) - encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) - }) - } -}*/ - -public final class PrivateCallScreen: OverlayMaskContainerView { +public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate { public struct State: Equatable { public struct SignalInfo: Equatable { public var quality: Double @@ -75,17 +31,28 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } public struct TerminatedState: Equatable { + public enum Reason { + case missed + case hangUp + case failed + case busy + case declined + } + public var duration: Double + public var reason: Reason - public init(duration: Double) { + public init(duration: Double, reason: Reason) { self.duration = duration + self.reason = reason } } public enum LifecycleState: Equatable { - case connecting + case requesting case ringing - case exchangingKeys + case connecting + case reconnecting case active(ActiveState) case terminated(TerminatedState) } @@ -100,7 +67,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var shortName: String public var avatarImage: UIImage? public var audioOutput: AudioOutput - public var isMicrophoneMuted: Bool + public var isLocalAudioMuted: Bool + public var isRemoteAudioMuted: Bool public var localVideo: VideoSource? public var remoteVideo: VideoSource? public var isRemoteBatteryLow: Bool @@ -111,7 +79,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { shortName: String, avatarImage: UIImage?, audioOutput: AudioOutput, - isMicrophoneMuted: Bool, + isLocalAudioMuted: Bool, + isRemoteAudioMuted: Bool, localVideo: VideoSource?, remoteVideo: VideoSource?, isRemoteBatteryLow: Bool @@ -121,7 +90,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.shortName = shortName self.avatarImage = avatarImage self.audioOutput = audioOutput - self.isMicrophoneMuted = isMicrophoneMuted + self.isLocalAudioMuted = isLocalAudioMuted + self.isRemoteAudioMuted = isRemoteAudioMuted self.localVideo = localVideo self.remoteVideo = remoteVideo self.isRemoteBatteryLow = isRemoteBatteryLow @@ -143,7 +113,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if lhs.audioOutput != rhs.audioOutput { return false } - if lhs.isMicrophoneMuted != rhs.isMicrophoneMuted { + if lhs.isLocalAudioMuted != rhs.isLocalAudioMuted { + return false + } + if lhs.isRemoteAudioMuted != rhs.isRemoteAudioMuted { return false } if lhs.localVideo !== rhs.localVideo { @@ -162,12 +135,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private struct Params: Equatable { var size: CGSize var insets: UIEdgeInsets + var interfaceOrientation: UIInterfaceOrientation var screenCornerRadius: CGFloat var state: State - init(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State) { + init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State) { self.size = size self.insets = insets + self.interfaceOrientation = interfaceOrientation self.screenCornerRadius = screenCornerRadius self.state = state } @@ -204,11 +179,17 @@ public final class PrivateCallScreen: OverlayMaskContainerView { private var activeLocalVideoSource: VideoSource? private var waitingForFirstLocalVideoFrameDisposable: Disposable? + private var isUpdating: Bool = false + private var canAnimateAudioLevel: Bool = false private var displayEmojiTooltip: Bool = false private var isEmojiKeyExpanded: Bool = false private var areControlsHidden: Bool = false private var swapLocalAndRemoteVideo: Bool = false + private var isPictureInPictureActive: Bool = false + + private var hideEmojiTooltipTimer: Foundation.Timer? + private var hideControlsTimer: Foundation.Timer? private var processedInitialAudioLevelBump: Bool = false private var audioLevelBump: Float = 0.0 @@ -224,6 +205,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { public var microhoneMuteAction: (() -> Void)? public var endCallAction: (() -> Void)? public var backAction: (() -> Void)? + public var closeAction: (() -> Void)? + public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)? + + private let pipView: PrivateCallPictureInPictureView + private var pipContentSource: AnyObject? + private var pipVideoCallViewController: UIViewController? + private var pipController: AVPictureInPictureController? public override init(frame: CGRect) { self.overlayContentsView = UIView() @@ -249,6 +237,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.backButtonView = BackButtonView(text: "Back") + self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize())) + super.init(frame: frame) self.clipsToBounds = true @@ -264,10 +254,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView { self.avatarTransformLayer.addSublayer(self.avatarLayer) self.layer.addSublayer(self.avatarTransformLayer) - /*let edgeTestLayer = EdgeTestLayer() - edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0)) - self.layer.addSublayer(edgeTestLayer)*/ - self.addSubview(self.videoContainerBackgroundView) self.overlayContentsView.mask = self.maskContents @@ -310,6 +296,27 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } self.backAction?() } + + self.buttonGroupView.closePressed = { [weak self] in + guard let self else { + return + } + self.closeAction?() + } + + if #available(iOS 16.0, *) { + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.view.addSubview(self.pipView) + self.pipView.frame = pipVideoCallViewController.view.bounds + self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.pipView.translatesAutoresizingMaskIntoConstraints = true + self.pipVideoCallViewController = pipVideoCallViewController + } + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(10.0 as NSNumber, forKey: "inputRadius") + self.overlayContentsView.layer.filters = [blurFilter] + } } public required init?(coder: NSCoder) { @@ -335,6 +342,39 @@ public final class PrivateCallScreen: OverlayMaskContainerView { return result } + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.isPictureInPictureActive = true + if !self.isUpdating { + self.update(transition: .easeInOut(duration: 0.2)) + } + } + + public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.isPictureInPictureActive = false + if !self.isUpdating { + let wereControlsHidden = self.areControlsHidden + self.areControlsHidden = true + self.update(transition: .immediate) + + if !wereControlsHidden { + self.areControlsHidden = false + self.update(transition: .spring(duration: 0.4)) + } + } + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil { + if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture { + restoreUIForPictureInPicture(completionHandler) + } else { + completionHandler(false) + } + } else { + completionHandler(false) + } + } + public func addIncomingAudioLevel(value: Float) { if self.canAnimateAudioLevel { self.targetAudioLevel = value @@ -385,8 +425,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) { - let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state) + public func beginPictureInPictureIfPossible() { + if let pipController = self.pipController, (self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil) { + pipController.startPictureInPicture() + } + } + + public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: Transition) { + let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state) if self.params == params { return } @@ -468,8 +514,20 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if let previousParams = self.params, case .active = params.state.lifecycleState { switch previousParams.state.lifecycleState { - case .connecting, .exchangingKeys, .ringing: - self.displayEmojiTooltip = true + case .requesting, .ringing, .connecting, .reconnecting: + if self.hideEmojiTooltipTimer == nil { + self.displayEmojiTooltip = true + + self.hideEmojiTooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + if self.displayEmojiTooltip { + self.displayEmojiTooltip = false + self.update(transition: .spring(duration: 0.4)) + } + }) + } default: break } @@ -487,6 +545,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } private func updateInternal(params: Params, transition: Transition) { + self.isUpdating = true + defer { + self.isUpdating = false + } + let genericAlphaTransition: Transition switch transition.animation { case .none: @@ -497,6 +560,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let backgroundFrame = CGRect(origin: CGPoint(), size: params.size) + let wideContentWidth: CGFloat + if params.size.width < 500.0 { + wideContentWidth = params.size.width - 44.0 * 2.0 + } else { + wideContentWidth = 400.0 + } + var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = [] if self.swapLocalAndRemoteVideo { if let activeLocalVideoSource = self.activeLocalVideoSource { @@ -515,6 +585,53 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } let havePrimaryVideo = !activeVideoSources.isEmpty + if havePrimaryVideo && self.hideControlsTimer == nil { + self.hideControlsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + if !self.areControlsHidden { + self.areControlsHidden = true + self.update(transition: .spring(duration: 0.4)) + } + }) + } + + if #available(iOS 16.0, *) { + if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController { + if self.pipController == nil { + let pipContentSource = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: self, contentViewController: pipVideoCallViewController) + let pipController = AVPictureInPictureController(contentSource: pipContentSource) + self.pipController = pipController + pipController.canStartPictureInPictureAutomaticallyFromInline = true + pipController.delegate = self + } + } else if let pipController = self.pipController { + self.pipController = nil + + if pipController.isPictureInPictureActive { + pipController.stopPictureInPicture() + } + + if self.isPictureInPictureActive { + self.isPictureInPictureActive = false + } + } + } + + self.pipView.isRenderingEnabled = self.isPictureInPictureActive + self.pipView.video = self.activeRemoteVideoSource ?? self.activeLocalVideoSource + if let pipVideoCallViewController = self.pipVideoCallViewController { + if let video = self.pipView.video, let currentOutput = video.currentOutput { + var rotatedResolution = currentOutput.resolution + let resolvedRotationAngle = currentOutput.rotationAngle + if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 { + rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) + } + pipVideoCallViewController.preferredContentSize = rotatedResolution + } + } + let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden let backgroundAspect: CGFloat = params.size.width / params.size.height @@ -528,11 +645,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let backgroundStateIndex: Int switch params.state.lifecycleState { - case .connecting: - backgroundStateIndex = 0 - case .ringing: - backgroundStateIndex = 0 - case .exchangingKeys: + case .requesting, .ringing, .connecting, .reconnecting: backgroundStateIndex = 0 case let .active(activeState): if activeState.signalInfo.quality <= 0.2 { @@ -547,20 +660,36 @@ public final class PrivateCallScreen: OverlayMaskContainerView { transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size)) + var isVideoButtonEnabled = false + switch params.state.lifecycleState { + case .active, .reconnecting: + isVideoButtonEnabled = true + default: + isVideoButtonEnabled = false + } + + var isTerminated = false + switch params.state.lifecycleState { + case .terminated: + isTerminated = true + default: + break + } + var buttons: [ButtonGroupView.Button] = [ - ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), action: { [weak self] in + ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), isEnabled: isVideoButtonEnabled && !isTerminated, action: { [weak self] in guard let self else { return } self.videoAction?() }), - ButtonGroupView.Button(content: .microphone(isMuted: params.state.isMicrophoneMuted), action: { [weak self] in + ButtonGroupView.Button(content: .microphone(isMuted: params.state.isLocalAudioMuted), isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } self.microhoneMuteAction?() }), - ButtonGroupView.Button(content: .end, action: { [weak self] in + ButtonGroupView.Button(content: .end, isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } @@ -568,14 +697,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { }) ] if self.activeLocalVideoSource != nil { - buttons.insert(ButtonGroupView.Button(content: .flipCamera, action: { [weak self] in + buttons.insert(ButtonGroupView.Button(content: .flipCamera, isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } self.flipCameraAction?() }), at: 0) } else { - buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), action: { [weak self] in + buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), isEnabled: !isTerminated, action: { [weak self] in guard let self else { return } @@ -584,17 +713,28 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } var notices: [ButtonGroupView.Notice] = [] - if params.state.isMicrophoneMuted { - notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off")) - } - if params.state.remoteVideo != nil && params.state.localVideo == nil { - notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "Your camera is turned off")) - } - if params.state.isRemoteBatteryLow { - notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low")) + if !isTerminated { + if params.state.isLocalAudioMuted { + notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), icon: "Call/CallToastMicrophone", text: "Your microphone is turned off")) + } + if params.state.isRemoteAudioMuted { + notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), icon: "Call/CallToastMicrophone", text: "\(params.state.shortName)'s microphone is turned off")) + } + if params.state.remoteVideo != nil && params.state.localVideo == nil { + notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), icon: "Call/CallToastCamera", text: "Your camera is turned off")) + } + if params.state.isRemoteBatteryLow { + notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), icon: "Call/CallToastBattery", text: "\(params.state.shortName)'s battery is low")) + } } - let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, notices: notices, transition: transition) + /*var displayClose = false + if case .terminated = params.state.lifecycleState { + displayClose = true + }*/ + let displayClose = false + + let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, minWidth: wideContentWidth, controlsHidden: currentAreControlsHidden, displayClose: displayClose, buttons: buttons, notices: notices, transition: transition) var expandedEmojiKeyRect: CGRect? if self.isEmojiKeyExpanded { @@ -632,7 +772,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition) + let emojiExpandedInfoSize = emojiExpandedInfoView.update(width: wideContentWidth, transition: emojiExpandedInfoTransition) let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize) emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height)) emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size)) @@ -855,7 +995,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerView.blurredContainerLayer.position = self.avatarTransformLayer.position videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds videoContainerView.blurredContainerLayer.opacity = 0.0 - videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) + videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate) Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale) Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale) } else { @@ -865,7 +1005,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size) videoContainerView.blurredContainerLayer.opacity = 0.0 - videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate) + videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate) } } @@ -875,7 +1015,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center) videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size)) videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0) - videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) + videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition) let alphaTransition: Transition switch transition.animation { @@ -897,8 +1037,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView { } } - alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0) - alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0) + let videoAlpha: CGFloat = self.isPictureInPictureActive ? 0.0 : 1.0 + alphaTransition.setAlpha(view: videoContainerView, alpha: videoAlpha) + alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: videoAlpha) } var removedVideoContainerIndices: [Int] = [] @@ -910,7 +1051,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) { let alphaTransition: Transition = genericAlphaTransition - videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) + videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition) transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center) transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size)) transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) @@ -949,7 +1090,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView { }) alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0) - videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition) + videoContainerView.update(size: params.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition) } } } @@ -1018,9 +1159,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let titleString: String switch params.state.lifecycleState { - case .terminated: + case let .terminated(terminatedState): self.titleView.contentMode = .center - titleString = "Call Ended" + + switch terminatedState.reason { + case .busy: + titleString = "Line Busy" + case .declined: + titleString = "Call Declined" + case .failed: + titleString = "Call Failed" + case .hangUp: + titleString = "Call Ended" + case .missed: + titleString = "Call Missed" + } genericAlphaTransition.setScale(layer: self.blobLayer, scale: 0.3) genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0) self.canAnimateAudioLevel = false @@ -1046,12 +1199,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView { let statusState: StatusView.State switch params.state.lifecycleState { - case .connecting: + case .requesting: statusState = .waiting(.requesting) + case .connecting: + statusState = .waiting(.connecting) + case .reconnecting: + statusState = .waiting(.reconnecting) case .ringing: statusState = .waiting(.ringing) - case .exchangingKeys: - statusState = .waiting(.generatingKeys) case let .active(activeState): statusState = .active(StatusView.ActiveState(startTimestamp: activeState.startTime, signalStrength: activeState.signalInfo.quality)) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index f0c69d9a5ec..a72251da55b 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -71,6 +71,8 @@ private final class AnimatableProperty { let timeFromStart = timestamp - animation.startTimestamp var t = max(0.0, timeFromStart / duration) switch curve { + case .linear: + break case .easeInOut: t = listViewAnimationCurveEaseInOut(t) case .spring: diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index 348fc553ecc..269f0a80d29 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -434,9 +434,9 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode { }) case let .openWebView(url, simple): self.controllerInteraction.openWebView(markupButton.title, url, simple, .generic) - case let .requestPeer(peerType, buttonId): + case let .requestPeer(peerType, buttonId, maxQuantity): if let message = self.message { - self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId) + self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId, maxQuantity) } } if dismissIfOnce { diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD index ea86d5a2029..e7a2f29507c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/UndoUI", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", + "//submodules/AccountContext", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 787e2780023..b787d4ce5d0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -15,6 +15,7 @@ import PeerInfoUI import UndoUI import ChatPresentationInterfaceState import ChatInputPanelNode +import AccountContext private enum SubscriberAction: Equatable { case join @@ -25,6 +26,9 @@ private enum SubscriberAction: Equatable { case unmuteNotifications case unpinMessages(Int) case hidePinnedMessages + case openChannel + case openGroup + case openChat } private func titleAndColorForAction(_ action: SubscriberAction, theme: PresentationTheme, strings: PresentationStrings) -> (String, UIColor) { @@ -45,11 +49,30 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat return (strings.Chat_PanelUnpinAllMessages, theme.chat.inputPanel.panelControlAccentColor) case .hidePinnedMessages: return (strings.Chat_PanelHidePinnedMessages, theme.chat.inputPanel.panelControlAccentColor) + case .openChannel: + return (strings.SavedMessages_OpenChannel, theme.chat.inputPanel.panelControlAccentColor) + case .openGroup: + return (strings.SavedMessages_OpenGroup, theme.chat.inputPanel.panelControlAccentColor) + case .openChat: + return (strings.SavedMessages_OpenChat, theme.chat.inputPanel.panelControlAccentColor) } } -private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? { - if case .pinnedMessages = interfaceState.subject { +private func actionForPeer(context: AccountContext, peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? { + if case let .replyThread(message) = interfaceState.chatLocation, message.messageId.peerId == context.account.peerId { + if let peer = interfaceState.savedMessagesTopicPeer { + if case let .channel(channel) = peer { + if case .broadcast = channel.info { + return .openChannel + } else { + return .openGroup + } + } else if case .legacyGroup = peer { + return .openGroup + } + } + return .openChat + } else if case .pinnedMessages = interfaceState.subject { var canManagePin = false if let channel = peer as? TelegramChannel { canManagePin = channel.hasPermission(.pinMessages) @@ -276,6 +299,10 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } case .hidePinnedMessages, .unpinMessages: self.interfaceInteraction?.unpinAllMessages() + case .openChannel, .openGroup, .openChat: + if let presentationInterfaceState = self.presentationInterfaceState, let savedMessagesTopicPeer = presentationInterfaceState.savedMessagesTopicPeer { + self.interfaceInteraction?.navigateToChat(savedMessagesTopicPeer.id) + } } } @@ -301,9 +328,9 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal) } - if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force { + if let context = self.context, let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force { - if let action = actionForPeer(peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) { + if let action = actionForPeer(context: context, peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) { let previousAction = self.action self.action = action let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 919dfad20f0..f006a5ca4d4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -491,9 +491,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if remainingCutoutHeight > 0.0 { cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) } - + var maximumNumberOfLines: Int = 12 + if isPreview { + maximumNumberOfLines = mediaAndFlags != nil ? 4 : 6 + } let textString = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: messageTheme.primaryTextColor, linkColor: incoming ? mainColor : messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true) - let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) textLayoutAndApply = textLayoutAndApplyValue remainingCutoutHeight -= textLayoutAndApplyValue.0.size.height @@ -580,7 +583,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var viewCount: Int? var dateReplies = 0 var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) - if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { + if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) || presentationData.isPreview { dateReactionsAndPeers = ([], []) } for attribute in message.attributes { @@ -595,7 +598,13 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) + let dateFormat: MessageTimestampStatusFormat + if presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, format: dateFormat, associatedData: associatedData) let statusType: ChatMessageDateAndStatusType if incoming { @@ -625,8 +634,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( context: context, presentationData: presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !isPreview, + impressionCount: !isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .trailingContent( @@ -869,26 +878,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { animation.animator.updateFrame(layer: self.transformContainer.layer, frame: CGRect(origin: CGPoint(), size: actualSize), completion: nil) let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom)) - - if displayLine { - let backgroundView: MessageInlineBlockBackgroundView - if let current = self.backgroundView { - backgroundView = current - animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) - backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: nil, animation: animation) - } else { - backgroundView = MessageInlineBlockBackgroundView() - self.backgroundView = backgroundView - backgroundView.frame = backgroundFrame - self.transformContainer.view.insertSubview(backgroundView, at: 0) - backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: nil, animation: .None) - } - } else { - if let backgroundView = self.backgroundView { - self.backgroundView = nil - backgroundView.removeFromSuperview() - } - } + var patternTopRightPosition = CGPoint() if let (inlineMediaValue, inlineMediaSize) = inlineMediaAndSize { var inlineMediaFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - inlineMediaSize.width, y: backgroundInsets.top + inlineMediaEdgeInset), size: inlineMediaSize) @@ -896,6 +886,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { inlineMediaFrame.origin.x = insets.left } + patternTopRightPosition.x = insets.right + inlineMediaSize.width - 6.0 + let inlineMedia: TransformImageNode var updateMedia = false if let current = self.inlineMedia { @@ -979,6 +971,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.transformContainer.addSubnode(title.textNode) title.textNode.frame = titleFrame + + title.textNode.displaysAsynchronously = !presentationData.isPreview } else { title.textNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) animation.animator.updatePosition(layer: title.textNode.layer, position: titleFrame.origin, completion: nil) @@ -1079,6 +1073,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.transformContainer.addSubnode(subtitle.textNode) subtitle.textNode.frame = subtitleFrame + + subtitle.textNode.displaysAsynchronously = !presentationData.isPreview } else { subtitle.textNode.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) animation.animator.updatePosition(layer: subtitle.textNode.layer, position: subtitleFrame.origin, completion: nil) @@ -1108,6 +1104,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.transformContainer.addSubnode(text.textNode) text.textNode.frame = textFrame + + text.textNode.displaysAsynchronously = !presentationData.isPreview } else { text.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) animation.animator.updatePosition(layer: text.textNode.layer, position: textFrame.origin, completion: nil) @@ -1122,6 +1120,21 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let item = contentDisplayOrder.first(where: { $0.item == .media }), let (contentMediaSize, contentMediaApply) = contentMediaSizeAndApply { let contentMediaFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: contentMediaSize) + var offsetPatternForMedia = false + if let index = contentLayoutOrder.firstIndex(where: { $0 == .media }), index != contentLayoutOrder.count - 1 { + for i in (index + 1) ..< contentLayoutOrder.count { + switch contentLayoutOrder[i] { + case .title, .subtitle, .text: + offsetPatternForMedia = true + default: + break + } + } + } + if offsetPatternForMedia { + patternTopRightPosition.y = contentMediaFrame.maxY + 6.0 + } + let contentMedia = contentMediaApply(animation, synchronousLoads) if self.contentMedia !== contentMedia { self.contentMedia?.removeFromSupernode() @@ -1291,6 +1304,38 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.view.removeGestureRecognizer(tapRecognizer) } } + + if displayLine { + var pattern: MessageInlineBlockBackgroundView.Pattern? + if let backgroundEmojiId = author?.backgroundEmojiId { + pattern = MessageInlineBlockBackgroundView.Pattern( + context: context, + fileId: backgroundEmojiId, + file: message.associatedMedia[MediaId( + namespace: Namespaces.Media.CloudFile, + id: backgroundEmojiId + )] as? TelegramMediaFile + ) + } + + let backgroundView: MessageInlineBlockBackgroundView + if let current = self.backgroundView { + backgroundView = current + animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) + backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: pattern, patternTopRightPosition: patternTopRightPosition, animation: animation) + } else { + backgroundView = MessageInlineBlockBackgroundView() + self.backgroundView = backgroundView + backgroundView.frame = backgroundFrame + self.transformContainer.view.insertSubview(backgroundView, at: 0) + backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: pattern, patternTopRightPosition: patternTopRightPosition, animation: .None) + } + } else { + if let backgroundView = self.backgroundView { + self.backgroundView = nil + backgroundView.removeFromSuperview() + } + } }) }) }) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 278e6603cb1..6238d518bdf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -121,6 +121,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ var needReactions = true + let hideAllAdditionalInfo = item.presentationData.isPreview + var hasSeparateCommentsButton = false outer: for (message, itemAttributes) in item.content { @@ -244,6 +246,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if let _ = media as? TelegramMediaGiveaway { result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false + } else if let _ = media as? TelegramMediaGiveawayResults { + result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false } else if let _ = media as? TelegramMediaUnsupported { isUnsupportedMedia = true needReactions = false @@ -333,13 +338,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ needReactions = false } - if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) { + if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) && !hideAllAdditionalInfo { if hasCommentButton(item: item) { result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .footer, neighborSpacing: .default))) } } - if !reactionsAreInline, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty { + if !reactionsAreInline && !hideAllAdditionalInfo, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty { if result.last?.1 == ChatMessageTextBubbleContentNode.self { } else { if result.last?.1 == ChatMessagePollBubbleContentNode.self || @@ -1415,7 +1420,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var allowAuthor = incoming - if let author = firstMessage.author, author is TelegramChannel, !incoming { + if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview { allowAuthor = true ignoreNameHiding = true } @@ -1962,7 +1967,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else { bubbleReactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) } - if !bubbleReactions.reactions.isEmpty { + if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { bottomNodeMergeStatus = .Both } @@ -2028,11 +2033,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI authorNameColor = color if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { + if effectiveAuthor is TelegramChannel, let emojiStatus = effectiveAuthor.emojiStatus { + currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: color.withMultipliedAlpha(0.4), loopMode: .count(2)) + } } else if effectiveAuthor.isScam { currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if effectiveAuthor.isFake { currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) - } else if let user = effectiveAuthor as? TelegramUser, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = effectiveAuthor.emojiStatus { currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: color.withMultipliedAlpha(0.4), loopMode: .count(2)) } else if effectiveAuthor.isVerified { currentCredibilityIcon = .verified(fillColor: item.presentationData.theme.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -2134,7 +2142,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } let dateFormat: MessageTimestampStatusFormat - if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { + if item.presentationData.isPreview { + dateFormat = .full + } else if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { dateFormat = .minimal } else { dateFormat = .regular @@ -2162,8 +2172,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let statusSuggestedWidthAndContinue = mosaicStatusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, presentationData: item.presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !item.presentationData.isPreview, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), @@ -2469,14 +2479,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var maxContentWidth: CGFloat = headerSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? - if let replyMarkup = replyMarkup { + if let replyMarkup = replyMarkup, !item.presentationData.isPreview { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? - if !bubbleReactions.reactions.isEmpty { + if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { var maximumNodeWidth = maximumNodeWidth if hasInstantVideo { maximumNodeWidth = min(309, baseWidth - 84) @@ -5745,4 +5755,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return (image, self.backgroundNode.frame) } + + public func isServiceLikeMessage() -> Bool { + for contentNode in self.contentNodes { + if contentNode is ChatMessageActionBubbleContentNode { + return true + } + } + return false + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index b62393f58e4..e1de4b5dc99 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -8,6 +8,7 @@ import LocalizedPeerData import AccountContext public enum MessageTimestampStatusFormat { + case full case regular case minimal } @@ -29,7 +30,38 @@ private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: Pres } } -public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData) -> String { +private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { + switch index { + case 0: + return strings.Month_ShortJanuary + case 1: + return strings.Month_ShortFebruary + case 2: + return strings.Month_ShortMarch + case 3: + return strings.Month_ShortApril + case 4: + return strings.Month_ShortMay + case 5: + return strings.Month_ShortJune + case 6: + return strings.Month_ShortJuly + case 7: + return strings.Month_ShortAugust + case 8: + return strings.Month_ShortSeptember + case 9: + return strings.Month_ShortOctober + case 10: + return strings.Month_ShortNovember + case 11: + return strings.Month_ShortDecember + default: + return "" + } +} + +public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData, ignoreAuthor: Bool = false) -> String { if let adAttribute = message.adAttribute { switch adAttribute.messageType { case .sponsored: @@ -50,7 +82,38 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess dateText = " " } - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { + var displayFullDate = false + if case .full = format, timestamp > 100000 { + displayFullDate = true + } else if let _ = message.forwardInfo, message.id.peerId == accountPeerId { + displayFullDate = true + } + + if displayFullDate { + let dayText: String + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var t: time_t = time_t(timestamp) + var timeinfo: tm = tm() + gmtime_r(&t, &timeinfo) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + if timeinfo.tm_year == timeinfoNow.tm_year { + if format != .full, timeinfo.tm_yday == timeinfoNow.tm_yday { + dayText = strings.Weekday_Today + } else { + dayText = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").string + } + } else { + dayText = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").string + } + dateText = strings.Message_FullDateFormat(dayText, stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)).string + } + else if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { dateText = strings.Message_ImportedDateFormat(dateStringForDay(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: forwardInfo.date), stringForMessageTimestamp(timestamp: forwardInfo.date, dateTimeFormat: dateTimeFormat), dateText).string } @@ -90,8 +153,13 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { authorTitle = nil } + if ignoreAuthor { + authorTitle = nil + } - if case .regular = format { + if case .minimal = format { + + } else { if let authorTitle = authorTitle, !authorTitle.isEmpty { dateText = "\(authorTitle), \(dateText)" } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 9ff63e0b985..427ed60fa74 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -289,6 +289,8 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } let textNode = textApply() + textNode.displaysAsynchronously = !presentationData.isPreview + if node.textNode == nil { textNode.isUserInteractionEnabled = false node.textNode = textNode diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 5b25714ce70..252c593418b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -252,30 +252,38 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { case let .giftPremium(_, _, monthsValue, _, _): months = monthsValue text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue): - giftSize.width += 34.0 - textSpacing += 13.0 - - if unclaimed { - title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed - } else { - title = item.presentationData.strings.Notification_PremiumPrize_Title - } - var peerName = "" - if let channelId, let channel = item.message.peers[channelId] { - peerName = EnginePeer(channel).compactDisplayTitle - } - if unclaimed { - text = item.presentationData.strings.Notification_PremiumPrize_UnclaimedText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string - } else if fromGiveaway { - text = item.presentationData.strings.Notification_PremiumPrize_GiveawayText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string + case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): + if channelId == nil { + months = monthsValue + text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if item.message.author?.id != item.context.account.peerId { + buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift + } } else { - text = item.presentationData.strings.Notification_PremiumPrize_GiftText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string + giftSize.width += 34.0 + textSpacing += 13.0 + + if unclaimed { + title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed + } else { + title = item.presentationData.strings.Notification_PremiumPrize_Title + } + var peerName = "" + if let channelId, let channel = item.message.peers[channelId] { + peerName = EnginePeer(channel).compactDisplayTitle + } + if unclaimed { + text = item.presentationData.strings.Notification_PremiumPrize_UnclaimedText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string + } else if fromGiveaway { + text = item.presentationData.strings.Notification_PremiumPrize_GiveawayText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string + } else { + text = item.presentationData.strings.Notification_PremiumPrize_GiftText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string + } + + months = monthsValue + buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View + hasServiceMessage = false } - - months = monthsValue - buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View - hasServiceMessage = false default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 3222c8b419c..c6fdb43aaa2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -24,7 +24,7 @@ private let titleFont = Font.medium(15.0) private let textFont = Font.regular(13.0) private let boldTextFont = Font.semibold(13.0) -public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode { +public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, UIGestureRecognizerDelegate { private let dateAndStatusNode: ChatMessageDateAndStatusNode private let placeholderNode: StickerShimmerEffectNode @@ -33,6 +33,12 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode private let prizeTitleNode: TextNode private let prizeTextNode: TextNode + private let additionalPrizeSeparatorNode: TextNode + private let additionalPrizeTextNode: TextNode + + private let additionalPrizeLeftLine: ASDisplayNode + private let additionalPrizeRightLine: ASDisplayNode + private let participantsTitleNode: TextNode private let participantsTextNode: TextNode @@ -81,6 +87,12 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode self.prizeTitleNode = TextNode() self.prizeTextNode = TextNode() + self.additionalPrizeSeparatorNode = TextNode() + self.additionalPrizeTextNode = TextNode() + + self.additionalPrizeLeftLine = ASDisplayNode() + self.additionalPrizeRightLine = ASDisplayNode() + self.participantsTitleNode = TextNode() self.participantsTextNode = TextNode() @@ -101,6 +113,10 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode self.addSubnode(self.prizeTitleNode) self.addSubnode(self.prizeTextNode) + self.addSubnode(self.additionalPrizeSeparatorNode) + self.addSubnode(self.additionalPrizeTextNode) + self.addSubnode(self.additionalPrizeLeftLine) + self.addSubnode(self.additionalPrizeRightLine) self.addSubnode(self.participantsTitleNode) self.addSubnode(self.participantsTextNode) self.addSubnode(self.countriesTextNode) @@ -134,7 +150,11 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + if case .user = peer { + item.controllerInteraction.openPeer(peer, .info(nil), nil, .default) + } else { + item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + } } } @@ -150,6 +170,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.bubbleTap(_:))) + tapRecognizer.delegate = self self.view.addGestureRecognizer(tapRecognizer) } @@ -158,6 +179,14 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode return true } + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let point = gestureRecognizer.location(in: self.view) + if case .ignore = self.tapActionAtPoint(point, gesture: .tap, isEstimating: false).content { + return false + } + return self.bounds.contains(point) + } + @objc private func bubbleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let item = self.item else { return @@ -181,6 +210,9 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let makePrizeTitleLayout = TextNode.asyncLayout(self.prizeTitleNode) let makePrizeTextLayout = TextNode.asyncLayout(self.prizeTextNode) + let makeAdditionalPrizeSeparatorLayout = TextNode.asyncLayout(self.additionalPrizeSeparatorNode) + let makeAdditionalPrizeTextLayout = TextNode.asyncLayout(self.additionalPrizeTextNode) + let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode) let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode) @@ -199,9 +231,12 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode return { item, layoutConstants, _, _, constrainedSize, _ in var giveaway: TelegramMediaGiveaway? + var giveawayResults: TelegramMediaGiveawayResults? for media in item.message.media { if let media = media as? TelegramMediaGiveaway { giveaway = media; + } else if let media = media as? TelegramMediaGiveawayResults { + giveawayResults = media } } @@ -217,6 +252,8 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first! let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor + let secondaryTextColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + let lineColor = secondaryTextColor.withMultipliedAlpha(0.2) let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor var badgeTextColor: UIColor = .white if badgeTextColor.distance(to: accentColor) < 1 { @@ -228,13 +265,57 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil) } - let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor) + let badgeText: String + if let giveaway { + badgeText = "X\(giveaway.quantity)" + } else if let giveawayResults { + badgeText = "X\(giveawayResults.winnersCount)" + } else { + badgeText = "" + } + let badgeString = NSAttributedString(string: badgeText, font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor) + + let prizeTitleText: String + if let giveawayResults { + if giveawayResults.winnersCount > 1 { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_Many + } else { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_One + } + } else { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle + } - let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor) + let prizeTitleString = NSAttributedString(string: prizeTitleText, font: titleFont, textColor: textColor) var prizeTextString: NSAttributedString? + var additionalPrizeSeparatorString: NSAttributedString? + var additionalPrizeTextString: NSAttributedString? if let giveaway { + var prizeDescription: String? + if let description = giveaway.prizeDescription { + prizeDescription = description + } + var trimSubscriptionCount = false + if let prizeDescription { + additionalPrizeSeparatorString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_With, font: textFont, textColor: secondaryTextColor) + additionalPrizeTextString = parseMarkdownIntoAttributedString("**\(giveaway.quantity)** \(prizeDescription)", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: textColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + trimSubscriptionCount = true + } + + var subscriptionsString = item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity) + if trimSubscriptionCount { + subscriptionsString = subscriptionsString.replacingOccurrences(of: "**\(giveaway.quantity)** ", with: "") + } + prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_PrizeText( - item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity), + subscriptionsString, item.presentationData.strings.Chat_Giveaway_Message_Months(giveaway.months) ).string, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), @@ -244,9 +325,38 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode return ("URL", url) } ), textAlignment: .center) + } else if let giveawayResults { + prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedText(giveawayResults.winnersCount), attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) } - let participantsTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle, font: titleFont, textColor: textColor) + var showWinners = false + if let giveawayResults, !giveawayResults.winnersPeerIds.isEmpty { + showWinners = true + } + + let participantsTitleText: String + if let giveawayResults { + if showWinners { + if giveawayResults.winnersCount > 1 { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_Many + } else { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_One + } + } else { + participantsTitleText = "" + } + } else { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle + } + + let participantsTitleString = NSAttributedString(string: participantsTitleText, font: titleFont, textColor: textColor) let participantsText: String let countriesText: String @@ -296,15 +406,26 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } let participantsTextString = NSAttributedString(string: participantsText, font: textFont, textColor: textColor) - let countriesTextString = NSAttributedString(string: countriesText, font: textFont, textColor: textColor) - let dateTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_DateTitle, font: titleFont, textColor: textColor) + var dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_DateTitle + if let giveawayResults { + if giveawayResults.winnersCount > giveawayResults.winnersPeerIds.count { + let moreCount = giveawayResults.winnersCount - Int32(giveawayResults.winnersPeerIds.count) + dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersMore(moreCount) + } else { + dateTitleText = "" + } + } + + let dateTitleString = NSAttributedString(string: dateTitleText, font: titleFont, textColor: textColor) var dateTextString: NSAttributedString? if let giveaway { dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor) + } else if let giveawayResults { + dateTextString = NSAttributedString(string: giveawayResults.winnersCount > 1 ? item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_Many : item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_One, font: textFont, textColor: textColor) } - let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, hidesHeaders: true) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0 @@ -313,7 +434,11 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let (badgeTextLayout, badgeTextApply) = makeBadgeTextLayout(TextNodeLayoutArguments(attributedString: badgeString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (prizeTitleLayout, prizeTitleApply) = makePrizeTitleLayout(TextNodeLayoutArguments(attributedString: prizeTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - + + let (additionalPrizeTextLayout, additionalPrizeTextApply) = makeAdditionalPrizeTextLayout(TextNodeLayoutArguments(attributedString: additionalPrizeTextString, backgroundColor: nil, maximumNumberOfLines: 6, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let (additionalPrizeSeparatorLayout, additionalPrizeSeparatorApply) = makeAdditionalPrizeSeparatorLayout(TextNodeLayoutArguments(attributedString: additionalPrizeSeparatorString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (prizeTextLayout, prizeTextApply) = makePrizeTextLayout(TextNodeLayoutArguments(attributedString: prizeTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (participantsTitleLayout, participantsTitleApply) = makeParticipantsTitleLayout(TextNodeLayoutArguments(attributedString: participantsTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -346,7 +471,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? switch position { @@ -377,7 +508,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode context: item.context, presentationData: item.presentationData, edited: edited, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), @@ -404,17 +535,21 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, item.presentationData.strings.Chat_Giveaway_Message_LearnMore.uppercased(), titleColor, false, true) - let months = giveaway?.months ?? 0 let animationName: String - switch months { - case 12: - animationName = "Gift12" - case 6: - animationName = "Gift6" - case 3: - animationName = "Gift3" - default: - animationName = "Gift3" + let months = giveaway?.months ?? 0 + if let _ = giveaway { + switch months { + case 12: + animationName = "Gift12" + case 6: + animationName = "Gift6" + case 3: + animationName = "Gift3" + default: + animationName = "Gift3" + } + } else { + animationName = "Celebrate" } var maxContentWidth: CGFloat = 0.0 @@ -423,21 +558,29 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } maxContentWidth = max(maxContentWidth, prizeTitleLayout.size.width) maxContentWidth = max(maxContentWidth, prizeTextLayout.size.width) + maxContentWidth = max(maxContentWidth, additionalPrizeSeparatorLayout.size.width) + maxContentWidth = max(maxContentWidth, additionalPrizeTextLayout.size.width) maxContentWidth = max(maxContentWidth, participantsTitleLayout.size.width) maxContentWidth = max(maxContentWidth, participantsTextLayout.size.width) maxContentWidth = max(maxContentWidth, dateTitleLayout.size.width) maxContentWidth = max(maxContentWidth, dateTextLayout.size.width) maxContentWidth = max(maxContentWidth, buttonWidth) - var channelPeers: [EnginePeer] = [] + var chipPeers: [EnginePeer] = [] if let channelPeerIds = giveaway?.channelPeerIds { for peerId in channelPeerIds { if let peer = item.message.peers[peerId] { - channelPeers.append(EnginePeer(peer)) + chipPeers.append(EnginePeer(peer)) + } + } + } else if let winnerPeerIds = giveawayResults?.winnersPeerIds { + for peerId in winnerPeerIds { + if let peer = item.message.peers[peerId] { + chipPeers.append(EnginePeer(peer)) } } } - let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1), incoming, item.presentationData.theme.theme.overallDarkAppearance) + let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, chipPeers, accentColor, accentColor.withAlphaComponent(0.1), incoming, item.presentationData.theme.theme.overallDarkAppearance) maxContentWidth = max(maxContentWidth, channelsWidth) maxContentWidth += 30.0 @@ -447,15 +590,30 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, 33.0) let buttonSpacing: CGFloat = 4.0 - let (channelButtonsSize, channelButtonsApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0) + let (channelButtonsSize, channelButtonsApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, !item.presentationData.isPreview) let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) - var layoutSize = CGSize(width: boundingWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 120.0) + let smallSpacing: CGFloat = 2.0 + let largeSpacing: CGFloat = 14.0 + + var layoutSize = CGSize(width: boundingWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTextLayout.size.height + dateTextLayout.size.height + 99.0) + if !item.presentationData.isPreview { + layoutSize.height += buttonSize.height + buttonSpacing + 7.0 + } + if additionalPrizeTextLayout.size.height > 0.0 { + layoutSize.height += additionalPrizeSeparatorLayout.size.height + additionalPrizeTextLayout.size.height + 7.0 + } if countriesTextLayout.size.height > 0.0 { layoutSize.height += countriesTextLayout.size.height + 7.0 } + if dateTitleLayout.size.height > 0.0 { + layoutSize.height += dateTitleLayout.size.height + } + if participantsTitleLayout.size.height > 0.0 { + layoutSize.height += participantsTitleLayout.size.height + largeSpacing + } layoutSize.height += channelButtonsSize.height if let statusSizeAndApply = statusSizeAndApply { @@ -467,16 +625,36 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode if let strongSelf = self { if strongSelf.item == nil { strongSelf.animationNode.autoplay = true + if let animationNode = strongSelf.animationNode as? DefaultAnimatedStickerNodeImpl, item.presentationData.isPreview { + animationNode.displaysAsynchronously = false + animationNode.forceSynchronous = true + } strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.start), mode: .direct(cachePathPrefix: nil)) } strongSelf.item = item strongSelf.giveaway = giveaway + let displaysAsynchronously = !item.presentationData.isPreview + strongSelf.badgeTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.prizeTitleNode.displaysAsynchronously = displaysAsynchronously + strongSelf.prizeTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.additionalPrizeTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.additionalPrizeSeparatorNode.displaysAsynchronously = displaysAsynchronously + strongSelf.participantsTitleNode.displaysAsynchronously = displaysAsynchronously + strongSelf.participantsTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.countriesTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.dateTitleNode.displaysAsynchronously = displaysAsynchronously + strongSelf.dateTextNode.displaysAsynchronously = displaysAsynchronously + strongSelf.buttonNode.titleNode.displaysAsynchronously = displaysAsynchronously + strongSelf.channelButtons.displaysAsynchronously = displaysAsynchronously + strongSelf.updateVisibility() - + let _ = badgeTextApply() let _ = prizeTitleApply() let _ = prizeTextApply() + let _ = additionalPrizeSeparatorApply() + let _ = additionalPrizeTextApply() let _ = participantsTitleApply() let _ = participantsTextApply() let _ = countriesTextApply() @@ -484,14 +662,14 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode let _ = dateTextApply() let _ = channelButtonsApply() let _ = buttonApply(animation) - - let smallSpacing: CGFloat = 2.0 - let largeSpacing: CGFloat = 14.0 - + + strongSelf.buttonNode.isHidden = item.presentationData.isPreview + var originY: CGFloat = 0.0 - let iconSize = CGSize(width: 140.0, height: 140.0) - strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY - 40.0), size: iconSize) + let animationOffset: CGFloat = giveaway != nil ? -40.0 : 15.0 + let iconSize = giveaway != nil ? CGSize(width: 140.0, height: 140.0) : CGSize(width: 84.0, height: 84.0) + strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - iconSize.width) / 2.0), y: originY + animationOffset), size: iconSize) strongSelf.animationNode.updateLayout(size: iconSize) let badgeTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - badgeTextLayout.size.width) / 2.0) + 1.0, y: originY + 88.0), size: badgeTextLayout.size) @@ -502,14 +680,43 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } originY += 112.0 - + strongSelf.prizeTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTitleLayout.size.width) / 2.0), y: originY), size: prizeTitleLayout.size) originY += prizeTitleLayout.size.height + smallSpacing + + if additionalPrizeTextLayout.size.height > 0.0 { + strongSelf.additionalPrizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - additionalPrizeTextLayout.size.width) / 2.0), y: originY), size: additionalPrizeTextLayout.size) + originY += additionalPrizeTextLayout.size.height + smallSpacing + + let separatorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - additionalPrizeSeparatorLayout.size.width) / 2.0), y: originY), size: additionalPrizeSeparatorLayout.size) + strongSelf.additionalPrizeSeparatorNode.frame = separatorFrame + originY += additionalPrizeSeparatorLayout.size.height + smallSpacing + + let lineSpacing: CGFloat = 7.0 + let lineWidth = (layoutSize.width - additionalPrizeSeparatorLayout.size.width - (27.0 + lineSpacing) * 2.0) / 2.0 + let lineHeight: CGFloat = 1.0 - UIScreenPixel + let lineSize = CGSize(width: lineWidth, height: lineHeight) + + strongSelf.additionalPrizeLeftLine.backgroundColor = lineColor + strongSelf.additionalPrizeLeftLine.isHidden = false + strongSelf.additionalPrizeLeftLine.frame = CGRect(origin: CGPoint(x: separatorFrame.minX - lineSize.width - lineSpacing, y: floorToScreenPixels(separatorFrame.midY - lineSize.height / 2.0)), size: lineSize) + + strongSelf.additionalPrizeRightLine.backgroundColor = lineColor + strongSelf.additionalPrizeRightLine.isHidden = false + strongSelf.additionalPrizeRightLine.frame = CGRect(origin: CGPoint(x: separatorFrame.maxX + lineSpacing, y: floorToScreenPixels(separatorFrame.midY - lineSize.height / 2.0)), size: lineSize) + } else { + strongSelf.additionalPrizeLeftLine.isHidden = true + strongSelf.additionalPrizeRightLine.isHidden = true + } + strongSelf.prizeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - prizeTextLayout.size.width) / 2.0), y: originY), size: prizeTextLayout.size) originY += prizeTextLayout.size.height + largeSpacing strongSelf.participantsTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTitleLayout.size.width) / 2.0), y: originY), size: participantsTitleLayout.size) - originY += participantsTitleLayout.size.height + smallSpacing + if participantsTitleLayout.size.height > 0.0 { + originY += participantsTitleLayout.size.height + smallSpacing + } + strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size) originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0 @@ -521,7 +728,9 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode strongSelf.countriesTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - countriesTextLayout.size.width) / 2.0), y: originY), size: countriesTextLayout.size) originY += countriesTextLayout.size.height } - originY += largeSpacing + if participantsTitleLayout.size.height > 0.0 { + originY += largeSpacing + } strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size) originY += dateTitleLayout.size.height + smallSpacing @@ -595,6 +804,12 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { return ChatMessageBubbleContentTapAction(content: .ignore) } + if self.prizeTextNode.frame.contains(point), let item = self.item { + if let giveawayResults = item.message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults { + item.controllerInteraction.navigateToMessageStandalone(giveawayResults.launchMessageId) + return ChatMessageBubbleContentTapAction(content: .ignore) + } + } return ChatMessageBubbleContentTapAction(content: .none) } @@ -649,7 +864,7 @@ private final class PeerButtonsStackNode: ASDisplayNode { var buttonNodes: [PeerButtonNode] = [] var openPeer: (EnginePeer) -> Void = { _ in } - static func asyncLayout(_ current: PeerButtonsStackNode) -> (_ context: AccountContext, _ width: CGFloat, _ peers: [EnginePeer], _ titleColor: UIColor, _ backgroundColor: UIColor, _ incoming: Bool, _ dark: Bool) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonsStackNode)) { + static func asyncLayout(_ current: PeerButtonsStackNode) -> (_ context: AccountContext, _ width: CGFloat, _ peers: [EnginePeer], _ titleColor: UIColor, _ backgroundColor: UIColor, _ incoming: Bool, _ dark: Bool) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonsStackNode)) { let currentChannelButtons = current.buttonNodes.isEmpty ? nil : current.buttonNodes let maybeMakeChannelButtons = current.buttonNodes.map(PeerButtonNode.asyncLayout) @@ -657,7 +872,7 @@ private final class PeerButtonsStackNode: ASDisplayNode { let targetNode = current var buttonNodes: [PeerButtonNode] = [] - let makeChannelButtonLayouts: [(_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode))] + let makeChannelButtonLayouts: [(_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonNode))] if let currentChannelButtons { buttonNodes = currentChannelButtons makeChannelButtonLayouts = maybeMakeChannelButtons @@ -677,7 +892,7 @@ private final class PeerButtonsStackNode: ASDisplayNode { var groups: [[Int]] = [] var currentGroup: [Int] = [] - var buttonContinues: [(CGFloat) -> (CGSize, () -> PeerButtonNode)] = [] + var buttonContinues: [(CGFloat, Bool) -> (CGSize, () -> PeerButtonNode)] = [] for i in 0 ..< makeChannelButtonLayouts.count { let peer = peers[i] let makeChannelButtonLayout = makeChannelButtonLayouts[i] @@ -739,13 +954,13 @@ private final class PeerButtonsStackNode: ASDisplayNode { originY += buttonHeight + verticalButtonSpacing } - return (maxWidth, { _ in + return (maxWidth, { _, displayAsynchronously in var buttonLayoutsAndApply: [(CGSize, () -> PeerButtonNode)] = [] for buttonApply in buttonContinues { - buttonLayoutsAndApply.append(buttonApply(maxWidth)) + buttonLayoutsAndApply.append(buttonApply(maxWidth, displayAsynchronously)) } - return (CGSize(width: maxWidth, height: originY - verticalButtonSpacing), { + return (CGSize(width: maxWidth, height: max(0, originY - verticalButtonSpacing)), { targetNode.buttonNodes = buttonNodes for i in 0 ..< buttonNodes.count { @@ -789,7 +1004,7 @@ private final class PeerButtonNode: HighlightTrackingButtonNode { self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) super.init() @@ -816,7 +1031,7 @@ private final class PeerButtonNode: HighlightTrackingButtonNode { self.pressed?() } - static func asyncLayout(_ current: PeerButtonNode?) -> (_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode)) { + static func asyncLayout(_ current: PeerButtonNode?) -> (_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat, Bool) -> (CGSize, () -> PeerButtonNode)) { let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout) return { context, width, peer, titleColor, backgroundColor in @@ -841,10 +1056,13 @@ private final class PeerButtonNode: HighlightTrackingButtonNode { let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: peer?.compactDisplayTitle ?? "", font: Font.medium(14.0), textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - avatarSize.width - (spacing + inset) * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets())) let refinedWidth = avatarSize.width + textSize.size.width + (spacing + inset) * 2.0 - return (refinedWidth, { _ in + return (refinedWidth, { _, displayAsynchronously in return (CGSize(width: refinedWidth, height: 24.0), { let _ = textApply() + targetNode.avatarNode.contentNode.displaysAsynchronously = displayAsynchronously + targetNode.textNode.displaysAsynchronously = displayAsynchronously + let backgroundFrame = CGRect(origin: .zero, size: CGSize(width: refinedWidth, height: 24.0)) let textFrame = CGRect(origin: CGPoint(x: inset + avatarSize.width + spacing, y: floorToScreenPixels((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size) targetNode.backgroundNode.frame = backgroundFrame diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 164ad096c67..6693dbfe762 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -746,7 +746,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { candidateTitleString = NSAttributedString(string: title ?? (arguments.file.fileName ?? "Unknown Track"), font: titleFont, textColor: arguments.customTintColor ?? messageTheme.fileTitleColor) let descriptionText: String if let performer = performer { - descriptionText = performer + descriptionText = performer.trimmingTrailingSpaces() } else if let size = arguments.file.size, size > 0 && size != .max { descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData)) } else { @@ -807,7 +807,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState? var displayTranscribe = false - if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !arguments.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) // MARK: Nicegram Speech2Text, nicegram premium check if arguments.associatedData.isPremium || isPremium() { @@ -938,7 +938,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var viewCount: Int? var dateReplies = 0 var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: arguments.associatedData.accountPeer, message: arguments.topMessage) - if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) { + if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) || arguments.presentationData.isPreview { dateReactionsAndPeers = ([], []) } for attribute in arguments.message.attributes { @@ -956,7 +956,13 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { edited = true } - let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings, associatedData: arguments.associatedData) + let dateFormat: MessageTimestampStatusFormat + if arguments.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings, format: dateFormat, associatedData: arguments.associatedData) let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message, isPremium: arguments.associatedData.isPremium, forceInline: arguments.associatedData.forceInlineReactions) var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings? @@ -975,8 +981,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: arguments.context, presentationData: arguments.presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !arguments.presentationData.isPreview, + impressionCount: !arguments.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: statusLayoutInput, @@ -1010,7 +1016,11 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 44.0 + 8.0 } + var statusHeightAddition: CGFloat = 0.0 if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + if statusSuggestedWidthAndContinue.0 > minLayoutWidth { + statusHeightAddition = 6.0 + } minLayoutWidth = max(minLayoutWidth, statusSuggestedWidthAndContinue.0) } @@ -1076,6 +1086,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusOffset = -10.0 fittedLayoutSize.height += statusOffset } + fittedLayoutSize.height += statusHeightAddition } } @@ -1279,7 +1290,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { if textString != nil { statusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width - 6.0 - statusSizeAndApply.0.width, y: textFrame.maxY + 4.0), size: statusSizeAndApply.0) } else { - statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0) + statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset + statusHeightAddition), size: statusSizeAndApply.0) } if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.dateAndStatusNode.frame = statusFrame @@ -1756,7 +1767,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { image = playerAlbumArt(postbox: context.account.postbox, engine: context.engine, fileReference: .message(message: MessageReference(message), media: file), albumArt: .init(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: false)), thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), drawPlaceholderWhenEmpty: false, attemptSynchronously: !animated) } } - let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor, image: image, overlayForegroundNodeColor: presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor) + let statusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor, image: image, overlayForegroundNodeColor: presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor) + if presentationData.isPreview { + statusNode.displaysAsynchronously = false + } self.statusNode = statusNode self.statusContainerNode.contentNode.insertSubnode(statusNode, at: 0) @@ -1843,7 +1857,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { cutoutFrame.origin.y += 6.0 } - if streamingState == .none && self.selectionNode == nil { + if (streamingState == .none && self.selectionNode == nil) || presentationData.isPreview { self.statusNode?.setCutout(nil, animated: animated) } else if let statusNode = self.statusNode, (self.iconNode?.isHidden ?? true) { statusNode.setCutout(cutoutFrame, animated: true) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 001fd935a91..46440364f07 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -268,6 +268,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { instantVideoBackgroundImage = nil ignoreHeaders = true } + if item.presentationData.isPreview { + ignoreHeaders = true + } if item.presentationData.theme != currentItem?.presentationData.theme { updatedInstantVideoBackgroundImage = instantVideoBackgroundImage @@ -523,13 +526,23 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData, ignoreAuthor: item.presentationData.isPreview) let maxDateAndStatusWidth: CGFloat if case .bubble = statusDisplayType { maxDateAndStatusWidth = width } else { - maxDateAndStatusWidth = width - videoFrame.midX - 85.0 + if item.presentationData.isPreview { + maxDateAndStatusWidth = width - videoFrame.midX - 65.0 + } else { + maxDateAndStatusWidth = width - videoFrame.midX - 85.0 + } } var isReplyThread = false @@ -540,8 +553,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let statusSuggestedWidthAndContinue = makeDateAndStatusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, presentationData: item.presentationData, - edited: edited && !sentViaBot, - impressionCount: viewCount, + edited: edited && !sentViaBot && !item.presentationData.isPreview, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), @@ -570,6 +583,8 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { contentSize.height += dateAndStatusSize.height + 2.0 contentSize.width = max(contentSize.width, dateAndStatusSize.width) dateAndStatusOverflow = true + } else if item.presentationData.isPreview { + contentSize.width += 44.0 } let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width))) @@ -667,7 +682,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let durationTextColor: UIColor switch statusDisplayType { case .free: - let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper) + let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper) durationTextColor = serviceColor.primaryText durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper)) case .bubble: @@ -686,9 +701,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { previousVideoNode = strongSelf.videoNode if let durationBlurColor = durationBlurColor { if let durationBackgroundNode = strongSelf.durationBackgroundNode { - durationBackgroundNode.updateColor(color: durationBlurColor.0, enableBlur: durationBlurColor.1, transition: .immediate) + durationBackgroundNode.updateColor(color: durationBlurColor.0, enableBlur: durationBlurColor.1 && !item.presentationData.isPreview, transition: .immediate) } else { - let durationBackgroundNode = NavigationBackgroundNode(color: durationBlurColor.0, enableBlur: durationBlurColor.1) + let durationBackgroundNode = NavigationBackgroundNode(color: durationBlurColor.0, enableBlur: durationBlurColor.1 && !item.presentationData.isPreview) strongSelf.durationBackgroundNode = durationBackgroundNode strongSelf.addSubnode(durationBackgroundNode) } @@ -750,6 +765,13 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { strongSelf.insertSubnode(strongSelf.secretVideoPlaceholderBackground, belowSubnode: videoNode) strongSelf.insertSubnode(strongSelf.secretVideoPlaceholder, belowSubnode: videoNode) } + } else if item.presentationData.isPreview { + let updatedSecretPlaceholderSignal = chatMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), videoReference: .message(message: MessageReference(item.message), media: telegramFile), synchronousLoad: true) + strongSelf.secretVideoPlaceholder.displaysAsynchronously = false + strongSelf.secretVideoPlaceholder.setSignal(updatedSecretPlaceholderSignal, attemptSynchronously: true) + if strongSelf.secretVideoPlaceholder.supernode == nil { + strongSelf.insertSubnode(strongSelf.secretVideoPlaceholder, aboveSubnode: videoNode) + } } updatedPlayerStatusSignal = videoNode.status @@ -785,7 +807,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } var displayTranscribe = false - if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free { + if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !item.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) if item.associatedData.isPremium { displayTranscribe = true @@ -861,8 +883,11 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let durationNode = strongSelf.durationNode { var durationFrame = CGRect(origin: CGPoint(x: displayVideoFrame.midX - 56.0 - 25.0 * scaleProgress, y: displayVideoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0)) + if item.presentationData.isPreview { + durationFrame.origin.x -= 9.0 + } - durationNode.isSeen = !notConsumed + durationNode.isSeen = !notConsumed || item.presentationData.isPreview let size = durationNode.size if let durationBackgroundNode = strongSelf.durationBackgroundNode, size.width > 1.0 { durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate) @@ -892,8 +917,12 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil) case let .constrained(_, right): var dateAndStatusFrame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) - if incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe { - dateAndStatusFrame.origin.x = audioTranscriptionButton.frame.maxX + 7.0 + if incoming { + if let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe { + dateAndStatusFrame.origin.x = audioTranscriptionButton.frame.maxX + 7.0 + } else if item.presentationData.isPreview { + dateAndStatusFrame.origin.x = displayVideoFrame.midX + 64.0 + } } animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil) } @@ -1140,7 +1169,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return } - let displayMute: Bool + var displayMute: Bool switch status.mediaStatus { case let .fetchStatus(fetchStatus): switch fetchStatus { @@ -1152,6 +1181,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { case .playbackStatus: displayMute = false } + if item.presentationData.isPreview { + displayMute = false + } if displayMute != (!self.infoBackgroundNode.alpha.isZero) { if displayMute { self.infoBackgroundNode.alpha = 1.0 @@ -1187,10 +1219,14 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else if isBuffering ?? false { progressRequired = true } + if item.presentationData.isPreview { + progressRequired = true + } if progressRequired { if self.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: item.presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor) + let statusNode = RadialStatusNode(backgroundNodeColor: item.presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor, isPreview: item.presentationData.isPreview) + statusNode.displaysAsynchronously = !item.presentationData.isPreview self.isUserInteractionEnabled = false self.statusNode = statusNode self.addSubnode(statusNode) @@ -1264,6 +1300,9 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { state = .none } } + if item.presentationData.isPreview { + state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) + } if let statusNode = self.statusNode { if state == .none { self.statusNode = nil @@ -1317,7 +1356,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { self.durationNode?.status = .single(nil) self.videoNode?.isHidden = isSecretMedia self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia - self.secretVideoPlaceholder.isHidden = !isSecretMedia + self.secretVideoPlaceholder.isHidden = !isSecretMedia && !item.presentationData.isPreview } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index dfaf277534e..006fd84bf54 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -394,7 +394,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var message: Message? private var attributes: ChatMessageEntryAttributes? private var media: Media? - private var themeAndStrings: (PresentationTheme, PresentationStrings, String)? + private var themeAndStrings: (PresentationTheme, PresentationStrings, String, Bool)? private var sizeCalculation: InteractiveMediaNodeSizeCalculation? private var wideLayout: Bool? private var automaticDownload: InteractiveMediaNodeAutodownloadMode? @@ -795,7 +795,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } case .themeSettings, .image: unboundSize = CGSize(width: 160.0, height: 240.0).fitted(CGSize(width: 240.0, height: 240.0)) - case .color, .gradient: + case .color, .gradient, .emoticon: unboundSize = CGSize(width: 128.0, height: 128.0) } } else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { @@ -865,8 +865,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: context, presentationData: presentationData, - edited: dateAndStatus.edited, - impressionCount: dateAndStatus.viewCount, + edited: dateAndStatus.edited && !presentationData.isPreview, + impressionCount: !presentationData.isPreview ? dateAndStatus.viewCount : nil, dateText: dateAndStatus.dateText, type: dateAndStatus.type, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), @@ -1258,7 +1258,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr uploading = true } - if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading { + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading && !presentationData.isPreview { updateVideoFile = file if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile { @@ -1317,7 +1317,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr switch wallpaper.content { case let .file(file, _, _, _, isTheme, _): if isTheme { - return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .file(FileMediaReference.message(message: MessageReference(message), media: file))) + return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .file(FileMediaReference.message(message: MessageReference(message), media: file)), synchronousLoad: synchronousLoad) } else { var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) }) if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" { @@ -1333,7 +1333,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } else { - return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true) + return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad) } } case let .image(representations): @@ -1344,6 +1344,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr return solidColorImage(color) case let .gradient(colors, rotation): return gradientImage(colors.map(UIColor.init(rgb:)), rotation: rotation ?? 0) + case .emoticon: + return solidColorImage(.black) } } @@ -1409,7 +1411,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr |> map { resourceStatus -> (MediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - case .themeSettings, .color, .gradient, .image: + case .themeSettings, .color, .gradient, .image, .emoticon: updatedStatusSignal = .single((.Local, nil)) } } @@ -1429,7 +1431,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.attributes = attributes strongSelf.media = media strongSelf.wideLayout = wideLayout - strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator) + strongSelf.themeAndStrings = (presentationData.theme.theme, presentationData.strings, dateTimeFormat.decimalSeparator, presentationData.isPreview) strongSelf.sizeCalculation = sizeCalculation strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload @@ -1723,7 +1725,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } private func updateStatus(animated: Bool) { - guard let (theme, strings, decimalSeparator) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, let attributes = self.attributes, var automaticPlayback = self.automaticPlayback, let wideLayout = self.wideLayout else { + guard let (theme, strings, decimalSeparator, isPreview) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, let attributes = self.attributes, var automaticPlayback = self.automaticPlayback, let wideLayout = self.wideLayout else { return } @@ -1801,6 +1803,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if isPreview { + progressRequired = true + } var radialStatusSize: CGFloat if isSecretMedia { @@ -1810,7 +1815,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if progressRequired { if self.statusNode == nil { - let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor) + let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor, isPreview: isPreview) + statusNode.displaysAsynchronously = !isPreview let imageSize = self.imageNode.bounds.size statusNode.frame = CGRect(origin: CGPoint(x: floor(imageSize.width / 2.0 - radialStatusSize / 2.0), y: floor(imageSize.height / 2.0 - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) self.statusNode = statusNode @@ -1861,6 +1867,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string, iconName: nil) } } + + let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() + var animated = animated if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(updatingMedia.progress), cancelEnabled: true, animateRotation: true) @@ -1898,6 +1907,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr automaticPlayback = false } + if isPreview { + muted = false + } + if let actualFetchStatus = self.actualFetchStatus { if automaticPlayback || message.forwardInfo != nil { fetchStatus = actualFetchStatus @@ -1910,9 +1923,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } - - let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() - + let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) var media = self.media @@ -2074,6 +2085,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if isPreview, let file = media as? TelegramMediaFile { + if let duration = file.duration, !file.isVideoSticker { + let durationString = file.isAnimated ? gifTitle : stringForDuration(Int32(duration), position: nil) + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: false, active: false) + } + state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) + } if isSecretMedia { let remainingTime: Int32? @@ -2117,6 +2135,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr animated = true } + if isPreview { + if case .play = state { + } else { + state = .none + } + } + statusNode.transitionToState(state, animated: animated, completion: { [weak statusNode] in if removeStatusNode { statusNode?.removeFromSupernode() @@ -2124,9 +2149,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr }) statusNode.backgroundNodeColor = backgroundColor } - if let badgeContent = badgeContent { + if var badgeContent = badgeContent { if self.badgeNode == nil { let badgeNode = ChatMessageInteractiveMediaBadge() + if isPreview { + badgeNode.durationNode.displaysAsynchronously = false + } var inset: CGFloat = 6.0 if let corners = self.currentImageArguments?.corners, case .Tail = corners.bottomLeft { @@ -2150,6 +2178,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr animated = false } + if isPreview { + mediaDownloadState = nil + if case let .mediaDownload(backgroundColor, foregroundColor, duration, _ ,_ , _) = badgeContent { + badgeContent = .text(inset: 0.0, backgroundColor: backgroundColor, foregroundColor: foregroundColor, text: NSAttributedString(string: duration), iconName: nil) + } + } self.badgeNode?.update(theme: theme, content: badgeContent, mediaDownloadState: mediaDownloadState, animated: animated) } else if let badgeNode = self.badgeNode { self.badgeNode = nil @@ -2167,7 +2201,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if displaySpoiler { if self.extendedMediaOverlayNode == nil { - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) + let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, enableAnimations: (self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) && !isPreview) extendedMediaOverlayNode.tapped = { [weak self] in self?.internallyVisible = true self?.updateVisibility() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index f04cc064961..ae698993b69 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -374,10 +374,10 @@ public final class ChatMessageAvatarHeader: ListViewItemHeader { public let effectiveTimestamp: Int32 public let presentationData: ChatPresentationData public let context: AccountContext - public let controllerInteraction: ChatControllerInteraction + public let controllerInteraction: ChatControllerInteraction? public let storyStats: PeerStoryStats? - public init(timestamp: Int32, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, message: Message, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, storyStats: PeerStoryStats?) { + public init(timestamp: Int32, peerId: PeerId, peer: Peer?, messageReference: MessageReference?, message: Message, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction?, storyStats: PeerStoryStats?) { self.peerId = peerId self.peer = peer self.messageReference = messageReference @@ -439,7 +439,7 @@ private let maxVideoLoopCount = 3 public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, ChatMessageAvatarHeaderNode { private let context: AccountContext private var presentationData: ChatPresentationData - private let controllerInteraction: ChatControllerInteraction + private let controllerInteraction: ChatControllerInteraction? private var storyStats: PeerStoryStats? private let peerId: PeerId @@ -469,7 +469,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat } } - public init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, adMessageId: EngineMessage.Id?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction, storyStats: PeerStoryStats?, synchronousLoad: Bool) { + public init(peerId: PeerId, peer: Peer?, messageReference: MessageReference?, adMessageId: EngineMessage.Id?, presentationData: ChatPresentationData, context: AccountContext, controllerInteraction: ChatControllerInteraction?, storyStats: PeerStoryStats?, synchronousLoad: Bool) { self.peerId = peerId self.peer = peer self.messageReference = messageReference @@ -482,6 +482,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat self.containerNode = ContextControllerSourceNode() self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.contentNode.displaysAsynchronously = !presentationData.isPreview super.init(layerBacked: false, dynamicBounce: true, isRotated: true, seeThrough: false) @@ -506,7 +507,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat if let messageReference = messageReference, case let .message(_, _, id, _, _, _) = messageReference.content { messageId = id } - strongSelf.controllerInteraction.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) + strongSelf.controllerInteraction?.openPeerContextMenu(peer, messageId, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) } self.updateSelectionState(animated: false) @@ -703,7 +704,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat } public func updateSelectionState(animated: Bool) { - let offset: CGFloat = self.controllerInteraction.selectionState != nil ? 42.0 : 0.0 + let offset: CGFloat = self.controllerInteraction?.selectionState != nil ? 42.0 : 0.0 let previousSubnodeTransform = self.subnodeTransform self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); @@ -727,15 +728,15 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat @objc private func tapGesture(_ recognizer: ListViewTapGestureRecognizer) { if case .ended = recognizer.state { if self.peerId.namespace == Namespaces.Peer.Empty, case let .message(_, _, id, _, _, _) = self.messageReference?.content { - self.controllerInteraction.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) + self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame) } else if let peer = self.peer { if let adMessageId = self.adMessageId { - self.controllerInteraction.activateAdAction(adMessageId) + self.controllerInteraction?.activateAdAction(adMessageId) } else { if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - self.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default) + self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default) } else { - self.controllerInteraction.openPeer(EnginePeer(peer), .info(nil), self.messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: self)) + self.controllerInteraction?.openPeer(EnginePeer(peer), .info(nil), self.messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: self)) } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD index 9f0577a6505..8f73fb8736c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/ChatMessageBackground", "//submodules/ContextUI", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index a8bb5342c67..fc3c136f6fe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -27,6 +27,7 @@ import BundleIconComponent import ChatMessageBackground import ContextUI import UndoUI +import MergedAvatarsNode private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) @@ -776,7 +777,7 @@ private final class ChannelItemComponent: Component { self.mergedAvatarsNode = mergedAvatarsNode } - mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0) + mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0) let avatarsSize = CGSize(width: avatarSize.width + 20.0, height: avatarSize.height) mergedAvatarsNode.updateLayout(size: avatarsSize) mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: avatarFrame.midX - avatarsSize.width / 2.0, y: avatarFrame.minY), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index f62cad25d1c..64e2a050f10 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -214,7 +214,13 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? switch position { @@ -261,7 +267,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { context: item.context, presentationData: item.presentationData, edited: edited, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 93f8ddd2a78..789d2c8e45e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -275,7 +275,13 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? switch preparePosition { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD index 175efd024c0..cbea42ae83c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index da7d147bd51..0fb5e22a96e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -15,6 +15,7 @@ import ChatMessageDateAndStatusNode import ChatMessageBubbleContentNode import ChatMessageItemCommon import PollBubbleTimerNode +import MergedAvatarsNode private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -976,7 +977,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) let statusType: ChatMessageDateAndStatusType? switch position { @@ -1440,10 +1447,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0) } - let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - defaultMergedImageSize) / 2.0)), size: CGSize(width: defaultMergedImageSize + defaultMergedImageSpacing * 2.0, height: defaultMergedImageSize)) + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize)) strongSelf.avatarsNode.frame = avatarsFrame strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) - strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: defaultMergedImageSize, imageSpacing: defaultMergedImageSpacing, borderWidth: defaultBorderWidth) + strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth) strongSelf.avatarsNode.isHidden = isBotChat let alphaTransition: ContainedViewLayoutTransition if animation.isAnimated { @@ -1706,202 +1713,3 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return nil } } - -private enum PeerAvatarReference: Equatable { - case letters(PeerId, PeerNameColor?, [String]) - case image(PeerReference, TelegramMediaImageRepresentation) - - var peerId: PeerId { - switch self { - case let .letters(value, _, _): - return value - case let .image(value, _): - return value.id - } - } -} - -private extension PeerAvatarReference { - init(peer: Peer) { - if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { - self = .image(peerReference, photo) - } else { - self = .letters(peer.id, peer.nameColor, peer.displayLetters) - } - } -} - -private final class MergedAvatarsNodeArguments: NSObject { - let peers: [PeerAvatarReference] - let images: [PeerId: UIImage] - let imageSize: CGFloat - let imageSpacing: CGFloat - let borderWidth: CGFloat - - init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { - self.peers = peers - self.images = images - self.imageSize = imageSize - self.imageSpacing = imageSpacing - self.borderWidth = borderWidth - } -} - -private let defaultMergedImageSize: CGFloat = 16.0 -private let defaultMergedImageSpacing: CGFloat = 15.0 -private let defaultBorderWidth: CGFloat = 1.0 - -private let avatarFont = avatarPlaceholderFont(size: 8.0) - -public final class MergedAvatarsNode: ASDisplayNode { - private var peers: [PeerAvatarReference] = [] - private var images: [PeerId: UIImage] = [:] - private var disposables: [PeerId: Disposable] = [:] - private let buttonNode: HighlightTrackingButtonNode - private var imageSize: CGFloat = defaultMergedImageSize - private var imageSpacing: CGFloat = defaultMergedImageSpacing - private var borderWidthValue: CGFloat = defaultBorderWidth - - public var pressed: (() -> Void)? - - override public init() { - self.buttonNode = HighlightTrackingButtonNode() - - super.init() - - self.isOpaque = false - self.displaysAsynchronously = true - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.buttonNode) - } - - deinit { - for (_, disposable) in self.disposables { - disposable.dispose() - } - } - - @objc private func buttonPressed() { - self.pressed?() - } - - public func updateLayout(size: CGSize) { - self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) - } - - public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { - self.imageSize = imageSize - self.imageSpacing = imageSpacing - self.borderWidthValue = borderWidth - var filteredPeers = peers.map(PeerAvatarReference.init) - if filteredPeers.count > 3 { - filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3) - } - if filteredPeers != self.peers { - self.peers = filteredPeers - - var validImageIds: [PeerId] = [] - for peer in filteredPeers { - if case .image = peer { - validImageIds.append(peer.peerId) - } - } - - var removedImageIds: [PeerId] = [] - for (id, _) in self.images { - if !validImageIds.contains(id) { - removedImageIds.append(id) - } - } - var removedDisposableIds: [PeerId] = [] - for (id, disposable) in self.disposables { - if !validImageIds.contains(id) { - disposable.dispose() - removedDisposableIds.append(id) - } - } - for id in removedImageIds { - self.images.removeValue(forKey: id) - } - for id in removedDisposableIds { - self.disposables.removeValue(forKey: id) - } - for peer in filteredPeers { - switch peer { - case let .image(peerReference, representation): - if self.disposables[peer.peerId] == nil { - if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) { - let disposable = (signal - |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in - guard let strongSelf = self else { - return - } - let image = imageVersions?.0 - if let image = image { - strongSelf.images[peer.peerId] = image - strongSelf.setNeedsDisplay() - } - }) - self.disposables[peer.peerId] = disposable - } - } - case .letters: - break - } - } - self.setNeedsDisplay() - } - } - - override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { - return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue) - } - - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - assertNotOnMainThread() - - let context = UIGraphicsGetCurrentContext()! - - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fill(bounds) - } - - guard let parameters = parameters as? MergedAvatarsNodeArguments else { - return - } - - let mergedImageSize = parameters.imageSize - let mergedImageSpacing = parameters.imageSpacing - - var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize - for i in (0 ..< parameters.peers.count).reversed() { - let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth)) - context.setBlendMode(.normal) - - context.saveGState() - switch parameters.peers[i] { - case let .letters(peerId, nameColor, letters): - context.translateBy(x: currentX, y: 0.0) - drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId, nameColor: nameColor) - context.translateBy(x: -currentX, y: 0.0) - case .image: - if let image = parameters.images[parameters.peers[i].peerId] { - context.translateBy(x: imageRect.midX, y: imageRect.midY) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -imageRect.midX, y: -imageRect.midY) - context.draw(image.cgImage!, in: imageRect) - } else { - context.setFillColor(UIColor.gray.cgColor) - context.fillEllipse(in: imageRect) - } - } - context.restoreGState() - currentX -= mergedImageSpacing - } - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 19eba9f9d5b..6991eff691c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -652,16 +652,19 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { node.previousMediaReference = updatedMediaReference - node.titleNode?.displaysAsynchronously = !arguments.presentationData.isPreview //node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview let titleNode = titleApply() + titleNode.displaysAsynchronously = !arguments.presentationData.isPreview + var textArguments: TextNodeWithEntities.Arguments? if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor, attemptSynchronous: attemptSynchronous) } let previousTextContents = node.textNode?.textNode.layer.contents let textNode = textApply(textArguments) + textNode.textNode.displaysAsynchronously = !arguments.presentationData.isPreview + textNode.visibilityRect = node.visibility ? CGRect.infinite : nil if node.titleNode == nil { @@ -768,7 +771,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if let current = node.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: arguments.context.sharedContext.energyUsageSettings.fullTranslucency) + dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: arguments.context.sharedContext.energyUsageSettings.fullTranslucency && !arguments.presentationData.isPreview) dustNode.isUserInteractionEnabled = false node.dustNode = dustNode node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode.textNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 1772b20bb70..d279d61542b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -602,7 +602,13 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } - let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData) + let dateFormat: MessageTimestampStatusFormat + if item.presentationData.isPreview { + dateFormat = .full + } else { + dateFormat = .regular + } + let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) var isReplyThread = false if case .replyThread = item.chatLocation { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 3e9e8bfe852..8a98990b747 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -242,7 +242,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let dateFormat: MessageTimestampStatusFormat - if let subject = item.associatedData.subject, case .messageOptions = subject { + if item.presentationData.isPreview { + dateFormat = .full + } else if let subject = item.associatedData.subject, case .messageOptions = subject { dateFormat = .minimal } else { dateFormat = .regular @@ -501,20 +503,46 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } attributedText = updatedString } - - let cutout: TextNodeCutout? = nil + + var customTruncationToken: NSAttributedString? + var maximumNumberOfLines: Int = 0 + if item.presentationData.isPreview { + if item.message.groupingKey != nil { + maximumNumberOfLines = 6 + } else if let image = item.message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, let dimensions = image.representations.first?.dimensions { + if dimensions.width > dimensions.height { + maximumNumberOfLines = 9 + } else { + maximumNumberOfLines = 6 + } + } else if let file = item.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isVideo || file.isAnimated, let dimensions = file.dimensions { + if dimensions.width > dimensions.height { + maximumNumberOfLines = 9 + } else { + maximumNumberOfLines = 6 + } + } else if let _ = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { + maximumNumberOfLines = 9 + } else { + maximumNumberOfLines = 12 + } + + let truncationToken = NSMutableAttributedString() + truncationToken.append(NSAttributedString(string: "\u{2026} ", font: textFont, textColor: messageTheme.primaryTextColor)) + truncationToken.append(NSAttributedString(string: item.presentationData.strings.Conversation_ReadMore, font: textFont, textColor: messageTheme.accentTextColor)) + customTruncationToken = truncationToken + } let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) - - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor)) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, customTruncationToken: customTruncationToken)) let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? if !textLayout.spoilers.isEmpty { - spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) + spoilerTextLayoutAndApply = spoilerTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true)) } else { spoilerTextLayoutAndApply = nil } - + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? if let statusType = statusType { var isReplyThread = false @@ -530,13 +558,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput - dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)) + dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: item.presentationData.isPreview ? nil : ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)) statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, presentationData: item.presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !item.presentationData.isPreview, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: dateLayoutInput, @@ -559,7 +587,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) - + var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) @@ -588,6 +616,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } + strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) let cachedLayout = strongSelf.textNode.textNode.cachedLayout @@ -633,7 +662,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) + dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency && !item.presentationData.isPreview) strongSelf.dustNode = dustNode strongSelf.containerNode.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift index dc835683d28..5792f86080c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -265,6 +265,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode } let fromYou = item.message.author?.id == item.context.account.peerId + let isChannel = item.message.id.peerId.isGroupOrChannel let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? "" let text: String @@ -281,7 +282,14 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode } } } else { - text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string + if item.associatedData.isRecentActions { + let authorName = item.message.author.flatMap { EnginePeer($0).compactDisplayTitle } ?? "" + text = item.presentationData.strings.Channel_AdminLog_ChannelChangedWallpaper(authorName).string + } else if item.message.id.peerId.isGroupOrChannel { + text = item.presentationData.strings.Notification_ChannelChangedWallpaper + } else { + text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string + } } let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor) @@ -311,15 +319,15 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode if displayTrailingAnimatedDots { textHeight += subtitleLayout.size.height } - let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou ? 0.0 : 42.0)) + let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou || isChannel ? 0.0 : 42.0)) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item - strongSelf.buttonNode.isHidden = fromYou - strongSelf.buttonTitleNode.isHidden = fromYou + strongSelf.buttonNode.isHidden = fromYou || isChannel + strongSelf.buttonTitleNode.isHidden = fromYou || isChannel let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize) if let media, mediaUpdated { @@ -327,7 +335,22 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode var imageSize = boundingSize let updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> var patternArguments: PatternWallpaperArguments? - switch media.content { + + var mediaContent = media.content + if case let .emoticon(emoticon) = mediaContent, let theme = item.associatedData.chatThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) { + let themeSettings: TelegramThemeSettings? + if let matching = theme.settings?.first(where: { $0.baseTheme == item.presentationData.theme.theme.referenceTheme.baseTheme }) { + themeSettings = matching + } else { + themeSettings = theme.settings?.first + } + + if let themeWallpaper = themeSettings?.wallpaper, let themeWallpaperContent = WallpaperPreviewMedia(wallpaper: themeWallpaper)?.content { + mediaContent = themeWallpaperContent + } + } + + switch mediaContent { case let .file(file, patternColors, rotation, intensity, _, _): var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(item.message), media: file).resourceReference($0.resource)) }) if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" { @@ -381,6 +404,8 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode updateImageSignal = gradientImage(colors.map(UIColor.init(rgb:)), rotation: rotation ?? 0) case .themeSettings: updateImageSignal = .complete() + case .emoticon: + updateImageSignal = .complete() } strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD index afcda60459f..6869a588736 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/TemporaryCachedPeerDataManager", "//submodules/UndoUI", "//submodules/WallpaperBackgroundNode", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index daac53664ac..ed49c75dfb9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -565,7 +565,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in - }, openRequestedPeerSelection: { _, _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index b932696754d..dfb8fb018f7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -9,6 +9,7 @@ import AccountContext import ChatControllerInteraction import ChatHistoryEntry import ChatMessageItemImpl +import TextFormat enum ChatRecentActionsEntryContentIndex: Int32 { case header = 0 @@ -56,6 +57,22 @@ private func appendAttributedText(text: PresentationStrings.FormattedString, gen string.append(text.string) } +private func appendAttributedText(text: PresentationStrings.FormattedString, additionalAttributes: inout [(NSRange, NSAttributedString.Key, Any)], generateEntities: (Int) -> ([MessageTextEntityType], [NSAttributedString.Key: Any]), to string: inout String, entities: inout [MessageTextEntity]) { + let nsString = string as NSString + for rangeItem in text.ranges { + let (types, additionalValues) = generateEntities(rangeItem.index) + for type in types { + entities.append(MessageTextEntity(range: (nsString.length + rangeItem.range.lowerBound) ..< (nsString.length + rangeItem.range.upperBound), type: type)) + } + let lowerBound = nsString.length + rangeItem.range.lowerBound + let range = NSRange(location: lowerBound, length: nsString.length + rangeItem.range.upperBound - lowerBound) + for (key, value) in additionalValues { + additionalAttributes.append((range, key, value)) + } + } + string.append(text.string) +} + private func appendAttributedText(text: String, withEntities: [MessageTextEntityType], to string: inout String, entities: inout [MessageTextEntity]) { for type in withEntities { entities.append(MessageTextEntity(range: string.count ..< (string.count + text.count), type: type)) @@ -69,7 +86,7 @@ private func filterOriginalMessageFlags(_ message: Message) -> Message { private func filterMessageChannelPeer(_ peer: Peer) -> Peer { if let peer = peer as? TelegramChannel { - return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId) + return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel) } return peer } @@ -146,7 +163,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: @@ -184,7 +201,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) + let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: @@ -234,7 +251,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) + let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: @@ -318,7 +335,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleSignatures(value): @@ -345,7 +362,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updatePinned(message): @@ -376,7 +393,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: @@ -415,7 +432,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -460,7 +477,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -503,7 +520,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -962,7 +979,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -992,7 +1009,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1079,7 +1096,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1152,7 +1169,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1179,7 +1196,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1210,7 +1227,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1247,7 +1264,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1281,7 +1298,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1310,7 +1327,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1341,7 +1358,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1367,7 +1384,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1393,7 +1410,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1419,7 +1436,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1450,7 +1467,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1481,7 +1498,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1552,7 +1569,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1583,7 +1600,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1623,7 +1640,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) @@ -1651,7 +1668,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .sendMessage(message): @@ -1676,7 +1693,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.headerStableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: @@ -1713,7 +1730,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .deleteTopic(info): @@ -1734,7 +1751,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .editTopic(prevInfo, newInfo): @@ -1807,7 +1824,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .pinTopic(prevInfo, newInfo): @@ -1845,7 +1862,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) } - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleForum(isForum): @@ -1866,7 +1883,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleAntiSpam(isEnabled): @@ -1887,73 +1904,220 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let action = TelegramMediaActionType.customText(text: text, entities: entities) + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) - - case let .changeNameColor(_, updatedValue): - var peers = SimpleDictionary() - var author: Peer? - if let peer = self.entry.peers[self.entry.event.peerId] { - author = peer - peers[peer.id] = peer - } + case let .changeNameColor(_, _, updatedColor, updatedIcon): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + let authorTitle = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" - var text: String = "" - var entities: [MessageTextEntity] = [] - - let _ = updatedValue + var text: String = "" + var entities: [MessageTextEntity] = [] + var additionalAttributes: [(NSRange, NSAttributedString.Key, Any)] = [] - let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedNameColorSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "●") + if let updatedIcon { + let rawText = self.presentationData.strings.Channel_AdminLog_ChannelChangedNameColorAndIcon(authorTitle, ".", ".") - appendAttributedText(text: rawText, generateEntities: { index in + let colors = context.peerNameColors.get(updatedColor) + var colorList: [UInt32] = [] + colorList.append(colors.main.argb) + if let secondary = colors.secondary { + colorList.append(secondary.argb) + } + if let tertiary = colors.tertiary { + colorList.append(tertiary.argb) + } + + appendAttributedText(text: rawText, additionalAttributes: &additionalAttributes, generateEntities: { index in if index == 0, let author = author { - return [.TextMention(peerId: author.id)] + return ([.TextMention(peerId: author.id)], [:]) } else if index == 1 { - return [.Bold] + return ([], [ + ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .nameColors(colorList)) + ]) + } else if index == 2 { + return ([.CustomEmoji(stickerPack: nil, fileId: updatedIcon)], [:]) + } else { + return ([], [:]) } - return [] }, to: &text, entities: &entities) + } else { + let rawText = self.presentationData.strings.Channel_AdminLog_ChannelChangedNameColor(authorTitle, ".") - let action = TelegramMediaActionType.customText(text: text, entities: entities) - - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) - case let .changeBackgroundEmojiId(_, updatedValue): - var peers = SimpleDictionary() - var author: Peer? - if let peer = self.entry.peers[self.entry.event.peerId] { - author = peer - peers[peer.id] = peer + let colors = context.peerNameColors.get(updatedColor) + var colorList: [UInt32] = [] + colorList.append(colors.main.argb) + if let secondary = colors.secondary { + colorList.append(secondary.argb) } + if let tertiary = colors.tertiary { + colorList.append(tertiary.argb) + } + + appendAttributedText(text: rawText, additionalAttributes: &additionalAttributes, generateEntities: { index in + if index == 0, let author = author { + return ([.TextMention(peerId: author.id)], [:]) + } else if index == 1 { + return ([], [ + ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .nameColors(colorList)) + ]) + } else { + return ([], [:]) + } + }, to: &text, entities: &entities) + } + + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: TelegramMediaActionType.CustomTextAttributes(attributes: additionalAttributes)) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .changeProfileColor(_, _, updatedColor, updatedIcon): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + let authorTitle = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" - var text: String = "" - var entities: [MessageTextEntity] = [] + var text: String = "" + var entities: [MessageTextEntity] = [] + var additionalAttributes: [(NSRange, NSAttributedString.Key, Any)] = [] + + if let updatedColor, let updatedIcon { + let rawText = self.presentationData.strings.Channel_AdminLog_ChannelChangedProfileColorAndIcon(authorTitle, ".", ".") - if let updatedValue, updatedValue != 0 { - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedBackgroundEmojiSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "."), generateEntities: { index in - if index == 0, let author = author { - return [.TextMention(peerId: author.id)] - } else if index == 1 { - return [.CustomEmoji(stickerPack: nil, fileId: updatedValue)] - } - return [] - }, to: &text, entities: &entities) - } else { - let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedBackgroundEmojiRemoved(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") - - appendAttributedText(text: rawText, generateEntities: { index in - if index == 0, let author = author { - return [.TextMention(peerId: author.id)] - } - return [] - }, to: &text, entities: &entities) + let colors = context.peerNameColors.get(updatedColor) + var colorList: [UInt32] = [] + colorList.append(colors.main.argb) + if let secondary = colors.secondary { + colorList.append(secondary.argb) + } + if let tertiary = colors.tertiary { + colorList.append(tertiary.argb) } - - let action = TelegramMediaActionType.customText(text: text, entities: entities) - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + appendAttributedText(text: rawText, additionalAttributes: &additionalAttributes, generateEntities: { index in + if index == 0, let author = author { + return ([.TextMention(peerId: author.id)], [:]) + } else if index == 1 { + return ([], [ + ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .nameColors(colorList)) + ]) + } else if index == 2 { + return ([.CustomEmoji(stickerPack: nil, fileId: updatedIcon)], [:]) + } else { + return ([], [:]) + } + }, to: &text, entities: &entities) + } else if let updatedColor { + let rawText = self.presentationData.strings.Channel_AdminLog_ChannelChangedProfileColor(authorTitle, ".") + + let colors = context.peerNameColors.get(updatedColor) + var colorList: [UInt32] = [] + colorList.append(colors.main.argb) + if let secondary = colors.secondary { + colorList.append(secondary.argb) + } + if let tertiary = colors.tertiary { + colorList.append(tertiary.argb) + } + + appendAttributedText(text: rawText, additionalAttributes: &additionalAttributes, generateEntities: { index in + if index == 0, let author = author { + return ([.TextMention(peerId: author.id)], [:]) + } else if index == 1 { + return ([], [ + ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .nameColors(colorList)) + ]) + } else { + return ([], [:]) + } + }, to: &text, entities: &entities) + } else { + let rawText = self.presentationData.strings.Channel_AdminLog_ChannelRemovedProfileColorAndIcon(authorTitle) + + appendAttributedText(text: rawText, additionalAttributes: &additionalAttributes, generateEntities: { index in + if index == 0, let author = author { + return ([.TextMention(peerId: author.id)], [:]) + } else { + return ([], [:]) + } + }, to: &text, entities: &entities) + } + + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: TelegramMediaActionType.CustomTextAttributes(attributes: additionalAttributes)) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .changeStatus(_, status): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + let authorTitle: String = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + + var text: String = "" + var entities: [MessageTextEntity] = [] + + if let status { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_ChannelUpdatedStatus(authorTitle, "."), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } else if index == 1 { + return [.CustomEmoji(stickerPack: nil, fileId: status.fileId)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_ChannelRemovedStatus(authorTitle), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .changeWallpaper(_, wallpaper): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + + let authorTitle: String = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + + var text: String = "" + var entities: [MessageTextEntity] = [] + + let action: TelegramMediaActionType + if let wallpaper { + action = TelegramMediaActionType.setChatWallpaper(wallpaper: wallpaper, forBoth: false) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_ChannelRemovedWallpaper(authorTitle), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) + } + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift index fd124ea0fa3..cbc53bc538f 100644 --- a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift @@ -256,6 +256,9 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { private let context: AccountContext private let presentationTheme: PresentationTheme + private let placeholder: String + private let shortPlaceholder: String? + private let theme: EditableTokenListNodeTheme private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode @@ -271,10 +274,13 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { public var deleteToken: ((AnyHashable) -> Void)? public var textReturned: (() -> Void)? - public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { + public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String, shortPlaceholder: String? = nil) { self.context = context self.presentationTheme = presentationTheme self.theme = theme + + self.placeholder = placeholder + self.shortPlaceholder = shortPlaceholder self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor) @@ -336,6 +342,18 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { public func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let validTokens = Set(tokens.map { $0.id }) + var placeholderSnapshot: UIView? + if let shortPlaceholder = self.shortPlaceholder { + let previousPlaceholder = self.placeholderNode.attributedText?.string ?? "" + let placeholder = validTokens.count > 0 ? shortPlaceholder : self.placeholder + + if !previousPlaceholder.isEmpty && placeholder != previousPlaceholder { + placeholderSnapshot = self.placeholderNode.layer.snapshotContentTreeAsView() + placeholderSnapshot?.frame = self.placeholderNode.frame + } + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor) + } + for i in (0 ..< self.tokenNodes.count).reversed() { let tokenNode = tokenNodes[i] if !validTokens.contains(tokenNode.token.id) { @@ -430,7 +448,22 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { currentOffset.y += 28.0 currentOffset.x = sideInset } - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize)) + + let previousPlaceholderWidth = self.placeholderNode.bounds.width + let placeholderFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize) + self.placeholderNode.bounds = CGRect(origin: .zero, size: placeholderSize) + transition.updatePosition(node: self.placeholderNode, position: placeholderFrame.center) + + if let placeholderSnapshot { + self.placeholderNode.view.superview?.insertSubview(placeholderSnapshot, belowSubview: self.placeholderNode.view) + self.placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + placeholderSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + placeholderSnapshot.removeFromSuperview() + }) + let delta = (placeholderSize.width - previousPlaceholderWidth) / 2.0 + transition.updatePosition(layer: placeholderSnapshot.layer, position: CGPoint(x: placeholderFrame.center.x - delta, y: placeholderFrame.center.y)) + transition.animatePositionAdditive(node: self.placeholderNode, offset: CGPoint(x: delta, y: 0.0)) + } let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0)) let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel)) diff --git a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD new file mode 100644 index 00000000000..1328ab6234f --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MergedAvatarsNode", + module_name = "MergedAvatarsNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift new file mode 100644 index 00000000000..184f5714294 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift @@ -0,0 +1,217 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import TelegramCore +import AvatarNode +import AccountContext + +private enum PeerAvatarReference: Equatable { + case letters(PeerId, PeerNameColor?, [String]) + case image(PeerReference, TelegramMediaImageRepresentation) + + var peerId: PeerId { + switch self { + case let .letters(value, _, _): + return value + case let .image(value, _): + return value.id + } + } +} + +private extension PeerAvatarReference { + init(peer: Peer) { + if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { + self = .image(peerReference, photo) + } else { + self = .letters(peer.id, peer.nameColor, peer.displayLetters) + } + } +} + +private final class MergedAvatarsNodeArguments: NSObject { + let peers: [PeerAvatarReference] + let images: [PeerId: UIImage] + let imageSize: CGFloat + let imageSpacing: CGFloat + let borderWidth: CGFloat + let avatarFontSize: CGFloat + + init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat) { + self.peers = peers + self.images = images + self.imageSize = imageSize + self.imageSpacing = imageSpacing + self.borderWidth = borderWidth + self.avatarFontSize = avatarFontSize + } +} + +private let defaultMergedImageSize: CGFloat = 16.0 +private let defaultMergedImageSpacing: CGFloat = 15.0 +private let defaultBorderWidth: CGFloat = 1.0 + +public final class MergedAvatarsNode: ASDisplayNode { + public static let defaultMergedImageSize: CGFloat = 16.0 + public static let defaultMergedImageSpacing: CGFloat = 15.0 + public static let defaultBorderWidth: CGFloat = 1.0 + public static let defaultAvatarFontSize: CGFloat = 8.0 + + private var peers: [PeerAvatarReference] = [] + private var images: [PeerId: UIImage] = [:] + private var disposables: [PeerId: Disposable] = [:] + private let buttonNode: HighlightTrackingButtonNode + private var imageSize: CGFloat = defaultMergedImageSize + private var imageSpacing: CGFloat = defaultMergedImageSpacing + private var borderWidthValue: CGFloat = defaultBorderWidth + private var avatarFontSize: CGFloat = defaultAvatarFontSize + + public var pressed: (() -> Void)? + + override public init() { + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.isOpaque = false + self.displaysAsynchronously = true + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.buttonNode) + } + + deinit { + for (_, disposable) in self.disposables { + disposable.dispose() + } + } + + @objc private func buttonPressed() { + self.pressed?() + } + + public func updateLayout(size: CGSize) { + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + } + + public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) { + self.imageSize = imageSize + self.imageSpacing = imageSpacing + self.borderWidthValue = borderWidth + self.avatarFontSize = avatarFontSize + + var filteredPeers = peers.map(PeerAvatarReference.init) + if filteredPeers.count > 3 { + filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3) + } + if filteredPeers != self.peers { + self.peers = filteredPeers + + var validImageIds: [PeerId] = [] + for peer in filteredPeers { + if case .image = peer { + validImageIds.append(peer.peerId) + } + } + + var removedImageIds: [PeerId] = [] + for (id, _) in self.images { + if !validImageIds.contains(id) { + removedImageIds.append(id) + } + } + var removedDisposableIds: [PeerId] = [] + for (id, disposable) in self.disposables { + if !validImageIds.contains(id) { + disposable.dispose() + removedDisposableIds.append(id) + } + } + for id in removedImageIds { + self.images.removeValue(forKey: id) + } + for id in removedDisposableIds { + self.disposables.removeValue(forKey: id) + } + for peer in filteredPeers { + switch peer { + case let .image(peerReference, representation): + if self.disposables[peer.peerId] == nil { + if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) { + let disposable = (signal + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in + guard let strongSelf = self else { + return + } + let image = imageVersions?.0 + if let image = image { + strongSelf.images[peer.peerId] = image + strongSelf.setNeedsDisplay() + } + }) + self.disposables[peer.peerId] = disposable + } + } + case .letters: + break + } + } + self.setNeedsDisplay() + } + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { + return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue, avatarFontSize: self.avatarFontSize) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + assertNotOnMainThread() + + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? MergedAvatarsNodeArguments else { + return + } + + let mergedImageSize = parameters.imageSize + let mergedImageSpacing = parameters.imageSpacing + + var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize + for i in (0 ..< parameters.peers.count).reversed() { + let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth)) + context.setBlendMode(.normal) + + context.saveGState() + switch parameters.peers[i] { + case let .letters(peerId, nameColor, letters): + context.translateBy(x: currentX, y: 0.0) + drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarPlaceholderFont(size: parameters.avatarFontSize), letters: letters, peerId: peerId, nameColor: nameColor) + context.translateBy(x: -currentX, y: 0.0) + case .image: + if let image = parameters.images[parameters.peers[i].peerId] { + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(image.cgImage!, in: imageRect) + } else { + context.setFillColor(UIColor.gray.cgColor) + context.fillEllipse(in: imageRect) + } + } + context.restoreGState() + currentX -= mergedImageSpacing + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift index 14f5bb3d832..9daa53bf3ae 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -334,7 +334,6 @@ private final class LineView: UIView { let _ = previousParams - self.backgroundView.tintColor = primaryColor if let secondaryColor { @@ -406,6 +405,10 @@ private final class LineView: UIView { self.dashBackgroundView = nil dashBackgroundView.removeFromSuperview() } + if let dashThirdBackgroundView = self.dashThirdBackgroundView { + self.dashThirdBackgroundView = nil + dashThirdBackgroundView.removeFromSuperview() + } self.backgroundView.alpha = 1.0 } @@ -456,6 +459,7 @@ public final class MessageInlineBlockBackgroundView: UIView { var thirdColor: UIColor? var backgroundColor: UIColor? var pattern: Pattern? + var patternTopRightPosition: CGPoint? var displayProgress: Bool init( @@ -466,6 +470,7 @@ public final class MessageInlineBlockBackgroundView: UIView { thirdColor: UIColor?, backgroundColor: UIColor?, pattern: Pattern?, + patternTopRightPosition: CGPoint?, displayProgress: Bool ) { self.size = size @@ -475,6 +480,7 @@ public final class MessageInlineBlockBackgroundView: UIView { self.thirdColor = thirdColor self.backgroundColor = backgroundColor self.pattern = pattern + self.patternTopRightPosition = patternTopRightPosition self.displayProgress = displayProgress } } @@ -605,6 +611,7 @@ public final class MessageInlineBlockBackgroundView: UIView { thirdColor: UIColor?, backgroundColor: UIColor?, pattern: Pattern?, + patternTopRightPosition: CGPoint? = nil, animation: ListViewItemUpdateAnimation ) { let params = Params( @@ -615,6 +622,7 @@ public final class MessageInlineBlockBackgroundView: UIView { thirdColor: thirdColor, backgroundColor: backgroundColor, pattern: pattern, + patternTopRightPosition: patternTopRightPosition, displayProgress: self.displayProgress ) if self.params == params { @@ -748,8 +756,14 @@ public final class MessageInlineBlockBackgroundView: UIView { } patternContentLayer.contents = self.patternContentsTarget?.contents + var patternOrigin = CGPoint(x: size.width, y: 0.0) + if let patternTopRightPosition { + patternOrigin.x -= patternTopRightPosition.x + patternOrigin.y += patternTopRightPosition.y + } + let itemSize = CGSize(width: placement.size / 3.0, height: placement.size / 3.0) - patternContentLayer.frame = CGRect(origin: CGPoint(x: size.width - placement.position.x / 3.0 - itemSize.width * 0.5, y: placement.position.y / 3.0 - itemSize.height * 0.5), size: itemSize) + patternContentLayer.frame = CGRect(origin: CGPoint(x: patternOrigin.x - placement.position.x / 3.0 - itemSize.width * 0.5, y: patternOrigin.y + placement.position.y / 3.0 - itemSize.height * 0.5), size: itemSize) var alphaFraction = abs(placement.position.x / 3.0) / min(500.0, size.width) alphaFraction = min(1.0, max(0.0, alphaFraction)) patternContentLayer.opacity = 0.3 * Float(1.0 - alphaFraction) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 60617ab3856..b8c212f020c 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -229,7 +229,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openJoinLink: (String) -> Void public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void public let activateAdAction: (EngineMessage.Id) -> Void - public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void + public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void public let saveMediaToFiles: (EngineMessage.Id) -> Void public let openNoAdsDemo: () -> Void public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void @@ -353,7 +353,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openJoinLink: @escaping (String) -> Void, openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void, activateAdAction: @escaping (EngineMessage.Id) -> Void, - openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void, + openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void, saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index a17dc48b20e..ccead170727 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2466,7 +2466,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, - layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), + layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: DeviceMetrics.iPhone12, isVisible: true, isExpanded: false diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift index 446862b82c4..822eacf1e8f 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/ChatTextInputMediaRecordingButton.swift @@ -445,6 +445,15 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM (self.micLockValue as? LockView)?.updateTheme(theme) } + public override func createLockPanelView() -> UIView! { + if self.hidesOnLock { + let view = WrapperBlurrredBackgroundView(frame: CGRect(origin: .zero, size: CGSize(width: 40.0, height: 72.0))) + return view + } else { + return super.createLockPanelView() + } + } + public func cancelRecording() { self.isEnabled = false self.isEnabled = true @@ -572,3 +581,31 @@ public final class ChatTextInputMediaRecordingButton: TGModernConversationInputM } } } + +private class WrapperBlurrredBackgroundView: UIView { + let view: BlurredBackgroundView + + override init(frame: CGRect) { + let view = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) + view.frame = CGRect(origin: .zero, size: frame.size) + view.update(size: frame.size, cornerRadius: frame.width / 2.0, transition: .immediate) + self.view = view + + super.init(frame: frame) + + self.addSubview(view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var frame: CGRect { + get { + return super.frame + } set { + super.frame = newValue + self.view.update(size: newValue.size, cornerRadius: newValue.width / 2.0, transition: .immediate) + } + } +} diff --git a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift index 637cd200492..06a736e3f5c 100644 --- a/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift +++ b/submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton/Sources/LockView.swift @@ -65,6 +65,7 @@ final class LockView: UIButton, TGModernConversationInputMicButtonLock { [ "Rectangle.Заливка 1": theme.chat.inputPanel.panelBackgroundColor, "Rectangle.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, + "Rectangle 2.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, "Path.Path.Обводка 1": theme.chat.inputPanel.panelControlAccentColor, "Path 4.Path 4.Обводка 1": theme.chat.inputPanel.panelControlAccentColor ].forEach { key, value in diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 5346f17a39c..d2f3799ef66 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -27,12 +27,65 @@ private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibol private let subtitleFont = Font.regular(13.0) public enum ChatTitleContent: Equatable { + public struct PeerData: Equatable { + public var peerId: PeerId + public var peer: Peer? + public var isContact: Bool + public var notificationSettings: TelegramPeerNotificationSettings? + public var peerPresences: [PeerId: PeerPresence] + public var cachedData: CachedPeerData? + + public init(peerId: PeerId, peer: Peer?, isContact: Bool, notificationSettings: TelegramPeerNotificationSettings?, peerPresences: [PeerId: PeerPresence], cachedData: CachedPeerData?) { + self.peerId = peerId + self.peer = peer + self.isContact = isContact + self.notificationSettings = notificationSettings + self.peerPresences = peerPresences + self.cachedData = cachedData + } + + public init(peerView: PeerView) { + self.init(peerId: peerView.peerId, peer: peerViewMainPeer(peerView), isContact: peerView.peerIsContact, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, peerPresences: peerView.peerPresences, cachedData: peerView.cachedData) + } + + public static func ==(lhs: PeerData, rhs: PeerData) -> Bool { + if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhs.peer == nil) != (rhs.peer == nil) { + return false + } + if lhs.isContact != rhs.isContact { + return false + } + if lhs.notificationSettings != rhs.notificationSettings { + return false + } + if lhs.peerPresences.count != rhs.peerPresences.count { + for (key, value) in lhs.peerPresences { + if let rhsValue = rhs.peerPresences[key] { + if !value.isEqual(to: rhsValue) { + return false + } + } else { + return false + } + } + } + if lhs.cachedData !== rhs.cachedData { + return false + } + return true + } + } + public enum ReplyThreadType { case comments case replies } - case peer(peerView: PeerView, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) + case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) case replyThread(type: ReplyThreadType, count: Int) case custom(String, String?, Bool) @@ -40,7 +93,7 @@ public enum ChatTitleContent: Equatable { switch lhs { case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount, isEnabled): if case let .peer(rhsPeerView, rhsCustomTitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount, rhsIsEnabled) = rhs { - if peerView !== rhsPeerView { + if peerView != rhsPeerView { return false } if customTitle != rhsCustomTitle { @@ -113,6 +166,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { public let titleLeftIconNode: ASImageNode public let titleRightIconNode: ASImageNode public let titleCredibilityIconView: ComponentHostView + public let titleVerifiedIconView: ComponentHostView public let activityNode: ChatTitleActivityNode private let button: HighlightTrackingButtonNode @@ -125,6 +179,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { private var titleLeftIcon: ChatTitleIcon = .none private var titleRightIcon: ChatTitleIcon = .none private var titleCredibilityIcon: ChatTitleCredibilityIcon = .none + private var titleVerifiedIcon: ChatTitleCredibilityIcon = .none private var presenceManager: PeerPresenceStatusManager? @@ -171,6 +226,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var titleLeftIcon: ChatTitleIcon = .none var titleRightIcon: ChatTitleIcon = .none var titleCredibilityIcon: ChatTitleCredibilityIcon = .none + var titleVerifiedIcon: ChatTitleCredibilityIcon = .none var isEnabled = true switch titleContent { case let .peer(peerView, customTitle, _, isScheduledMessages, isMuted, _, isEnabledValue): @@ -186,13 +242,13 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } isEnabled = false } else { - if let peer = peerViewMainPeer(peerView) { + if let peer = peerView.peer { if let customTitle = customTitle { segments = [.text(0, NSAttributedString(string: customTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else if peerView.peerId == self.context.account.peerId { segments = [.text(0, NSAttributedString(string: self.strings.Conversation_SavedMessages, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { - if !peerView.peerIsContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { + if !peerView.isContact, let user = peer as? TelegramUser, !user.flags.contains(.isSupport), user.botInfo == nil, let phone = user.phone, !phone.isEmpty { segments = [.text(0, NSAttributedString(string: formatPhoneNumber(context: self.context, number: phone), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else { segments = [.text(0, NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] @@ -204,7 +260,10 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityIcon = .fake } else if peer.isScam { titleCredibilityIcon = .scam - } else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { + if peer is TelegramChannel, peer.isVerified { + titleVerifiedIcon = .verified + } titleCredibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { titleCredibilityIcon = .verified @@ -221,7 +280,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleRightIcon = .mute } } else { - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if let notificationSettings = peerView.notificationSettings { if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { if titleCredibilityIcon != .verified { titleRightIcon = .mute @@ -328,18 +387,11 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if titleCredibilityIcon != self.titleCredibilityIcon { self.titleCredibilityIcon = titleCredibilityIcon - /*switch titleCredibilityIcon { - case .none: - self.titleCredibilityIconNode.image = nil - case .fake: - self.titleCredibilityIconNode.image = PresentationResourcesChatList.fakeIcon(titleTheme, strings: self.strings, type: .regular) - case .scam: - self.titleCredibilityIconNode.image = PresentationResourcesChatList.scamIcon(titleTheme, strings: self.strings, type: .regular) - case .verified: - self.titleCredibilityIconNode.image = PresentationResourcesChatList.verifiedIcon(titleTheme) - case .premium: - self.titleCredibilityIconNode.image = PresentationResourcesChatList.premiumIcon(titleTheme) - }*/ + updated = true + } + + if titleVerifiedIcon != self.titleVerifiedIcon { + self.titleVerifiedIcon = titleVerifiedIcon updated = true } @@ -371,7 +423,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if let titleContent = self.titleContent { switch titleContent { case let .peer(peerView, _, _, isScheduledMessages, _, _, _): - if let peer = peerViewMainPeer(peerView) { + if let peer = peerView.peer { if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { inputActivitiesAllowed = false } @@ -475,7 +527,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { if let customMessageCount = customMessageCount, customMessageCount != 0 { let string = NSAttributedString(string: self.strings.Conversation_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) - } else if let peer = peerViewMainPeer(peerView) { + } else if let peer = peerView.peer { let servicePeer = isServicePeer(peer) if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isReplies { let string = NSAttributedString(string: "", font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) @@ -496,7 +548,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { let string = NSAttributedString(string: statusText, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) - } else if let peer = peerViewMainPeer(peerView) { + } else if let peer = peerView.peer { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 let userPresence: TelegramUserPresence if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { @@ -643,6 +695,9 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.titleCredibilityIconView = ComponentHostView() self.titleCredibilityIconView.isUserInteractionEnabled = false + self.titleVerifiedIconView = ComponentHostView() + self.titleVerifiedIconView.isUserInteractionEnabled = false + self.activityNode = ChatTitleActivityNode() self.button = HighlightTrackingButtonNode() @@ -668,13 +723,16 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { strongSelf.titleTextNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleCredibilityIconView.layer.removeAnimation(forKey: "opacity") + strongSelf.titleVerifiedIconView.layer.removeAnimation(forKey: "opacity") strongSelf.titleTextNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 strongSelf.titleCredibilityIconView.alpha = 0.4 + strongSelf.titleVerifiedIconView.alpha = 0.4 } else { strongSelf.titleTextNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 strongSelf.titleCredibilityIconView.alpha = 1.0 + strongSelf.titleVerifiedIconView.alpha = 1.0 strongSelf.titleTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } @@ -703,6 +761,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { let titleContent = self.titleContent self.titleCredibilityIcon = .none + self.titleVerifiedIcon = .none self.titleContent = titleContent let _ = self.updateStatus() @@ -721,6 +780,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var leftIconWidth: CGFloat = 0.0 var rightIconWidth: CGFloat = 0.0 var credibilityIconWidth: CGFloat = 0.0 + var verifiedIconWidth: CGFloat = 0.0 if let image = self.titleLeftIconNode.image { if self.titleLeftIconNode.supernode == nil { @@ -747,6 +807,22 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) } + let titleVerifiedContent: EmojiStatusComponent.Content + switch self.titleVerifiedIcon { + case .none: + titleVerifiedContent = .none + case .premium: + titleVerifiedContent = .premium(color: self.theme.list.itemAccentColor) + case .verified: + titleVerifiedContent = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + case .fake: + titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) + case .scam: + titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) + case let .emojiStatus(emojiStatus): + titleVerifiedContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) + } + let titleCredibilitySize = self.titleCredibilityIconView.update( transition: .immediate, component: AnyComponent(EmojiStatusComponent( @@ -761,6 +837,20 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { containerSize: CGSize(width: 20.0, height: 20.0) ) + let titleVerifiedSize = self.titleVerifiedIconView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: titleVerifiedContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + if self.titleCredibilityIcon != .none { self.titleTextNode.view.addSubview(self.titleCredibilityIconView) credibilityIconWidth = titleCredibilitySize.width + 3.0 @@ -770,6 +860,15 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } } + if self.titleVerifiedIcon != .none { + self.titleTextNode.view.addSubview(self.titleVerifiedIconView) + verifiedIconWidth = titleVerifiedSize.width + 3.0 + } else { + if self.titleVerifiedIconView.superview != nil { + self.titleVerifiedIconView.removeFromSuperview() + } + } + if let image = self.titleRightIconNode.image { if self.titleRightIconNode.supernode == nil { self.titleTextNode.addSubnode(self.titleRightIconNode) @@ -787,8 +886,9 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { let titleSideInset: CGFloat = 6.0 var titleFrame: CGRect if size.height > 40.0 { - var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: titleTransition.isAnimated) + var titleSize = self.titleTextNode.updateLayout(size: CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - verifiedIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height), animated: titleTransition.isAnimated) titleSize.width += credibilityIconWidth + titleSize.width += verifiedIconWidth let activitySize = self.activityNode.updateLayout(CGSize(width: clearBounds.size.width - titleSideInset * 2.0, height: clearBounds.size.height), alignment: .center) let titleInfoSpacing: CGFloat = 0.0 @@ -821,30 +921,48 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size) } - self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: titleFrame.width - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + var nextIconX: CGFloat = titleFrame.width + + self.titleVerifiedIconView.frame = CGRect(origin: CGPoint(x: titleFrame.width - titleVerifiedSize.width, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize) + nextIconX -= titleVerifiedSize.width + if !titleVerifiedSize.width.isZero { + nextIconX -= 2.0 + } + + self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + nextIconX -= titleCredibilitySize.width if let image = self.titleRightIconNode.image { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0 + UIScreenPixel, y: 6.0), size: image.size) } } else { - let titleSize = self.titleTextNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: titleTransition.isAnimated) + let titleSize = self.titleTextNode.updateLayout(size: CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - verifiedIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height), animated: titleTransition.isAnimated) let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center) let titleInfoSpacing: CGFloat = 8.0 - let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing + let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + verifiedIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) titleTransition.updateFrameAdditiveToCenter(view: self.titleContainerView, frame: titleFrame) titleTransition.updateFrameAdditiveToCenter(node: self.titleTextNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) - self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) + self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + verifiedIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) if let image = self.titleLeftIconNode.image { self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size) } - self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + var nextIconX: CGFloat = titleFrame.maxX + + self.titleVerifiedIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleVerifiedSize.width, y: floor((titleFrame.height - titleVerifiedSize.height) / 2.0)), size: titleVerifiedSize) + nextIconX -= titleVerifiedSize.width + if !titleVerifiedSize.width.isZero { + nextIconX -= 2.0 + } + + self.titleCredibilityIconView.frame = CGRect(origin: CGPoint(x: nextIconX - titleCredibilitySize.width, y: floor((titleFrame.height - titleCredibilitySize.height) / 2.0)), size: titleCredibilitySize) + nextIconX -= titleCredibilitySize.width if let image = self.titleRightIconNode.image { self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width, y: titleFrame.minY + 6.0), size: image.size) diff --git a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift index be2e5f3e761..9e046f59877 100644 --- a/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift +++ b/submodules/TelegramUI/Components/ContextMenuScreen/Sources/ContextMenuNode.swift @@ -64,6 +64,7 @@ private final class ArrowNode: HighlightTrackingButtonNode { } final class ContextMenuNode: ASDisplayNode { + private let blurred: Bool private let isDark: Bool private let actions: [ContextMenuAction] @@ -93,6 +94,7 @@ final class ContextMenuNode: ASDisplayNode { private let feedback: HapticFeedback? init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, dismissOnTap: @escaping (UIView, CGPoint) -> Bool, catchTapsOutside: Bool, hasHapticFeedback: Bool, blurred: Bool = false, isDark: Bool = true) { + self.blurred = blurred self.isDark = isDark self.actions = actions @@ -264,9 +266,11 @@ final class ContextMenuNode: ASDisplayNode { self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4) } - self.allowsGroupOpacity = true - self.layer.rasterizationScale = UIScreen.main.scale - self.layer.shouldRasterize = true + if !(self.blurred && self.isDark) { + self.allowsGroupOpacity = true + self.layer.rasterizationScale = UIScreen.main.scale + self.layer.shouldRasterize = true + } self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in self?.allowsGroupOpacity = false self?.layer.shouldRasterize = false @@ -278,9 +282,11 @@ final class ContextMenuNode: ASDisplayNode { } func animateOut(bounce: Bool, completion: @escaping () -> Void) { - self.allowsGroupOpacity = true - self.layer.rasterizationScale = UIScreen.main.scale - self.layer.shouldRasterize = true + if !(self.blurred && self.isDark) { + self.allowsGroupOpacity = true + self.layer.rasterizationScale = UIScreen.main.scale + self.layer.shouldRasterize = true + } self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in self?.allowsGroupOpacity = false self?.layer.shouldRasterize = false diff --git a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal index 152adb1c90e..0f9f8fee11a 100644 --- a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal +++ b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal @@ -93,13 +93,9 @@ kernel void dustEffectUpdateParticle( float particleXFraction = float(particleX) / float(size.x); float particleFraction = particleEaseInValueAt(effectFraction, particleXFraction); - //Loki rng = Loki(gid, uint(phase * timeStep)); - //float2 offsetNorm = float2(1.0, 1.0) * 10.0 * timeStep; - Particle particle = particles[gid]; particle.offsetFromBasePosition += (particle.velocity * timeStep) * particleFraction; - //particle.velocity += ((-offsetNorm) * 0.5 + float2(rng.rand(), rng.rand()) * offsetNorm) * particleFraction; - //particle.velocity = particle.velocity * (1.0 - particleFraction) + particle.velocity * 1.001 * particleFraction; + particle.velocity += float2(0.0, timeStep * 120.0) * particleFraction; particle.lifetime = max(0.0, particle.lifetime - timeStep * particleFraction); particles[gid] = particle; diff --git a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift index 3cc57e5adb7..fec29202b3e 100644 --- a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift +++ b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift @@ -162,7 +162,6 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject } self.lastTimeStep = deltaTimeValue - //print("updateItems: \(deltaTime), localDeltaTime: \(localDeltaTime)") var didRemoveItems = false for i in (0 ..< self.items.count).reversed() { @@ -224,19 +223,6 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject if item.particleBuffer == nil { if let particleBuffer = MetalEngine.shared.sharedBuffer(spec: BufferSpec(length: particleCount * 4 * (4 + 1))) { item.particleBuffer = particleBuffer - - /*let particles = particleBuffer.buffer.contents().assumingMemoryBound(to: Float.self) - for i in 0 ..< particleCount { - particles[i * 5 + 0] = 0.0; - particles[i * 5 + 1] = 0.0; - - let direction = Float.random(in: 0.0 ..< Float.pi * 2.0) - let velocity = Float.random(in: 0.1 ... 0.2) * 420.0 - particles[i * 5 + 2] = cos(direction) * velocity - particles[i * 5 + 3] = sin(direction) * velocity - - particles[i * 5 + 4] = Float.random(in: 0.7 ... 1.5) - }*/ } } } @@ -279,6 +265,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject var phase = item.phase computeEncoder.setBytes(&phase, length: 4, index: 2) var timeStep: Float = Float(lastTimeStep) / Float(UIView.animationDurationFactor()) + timeStep *= 2.0 computeEncoder.setBytes(&timeStep, length: 4, index: 3) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) } diff --git a/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD b/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD new file mode 100644 index 00000000000..a6c83e6f0ee --- /dev/null +++ b/submodules/TelegramUI/Components/DynamicCornerRadiusView/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DynamicCornerRadiusView", + module_name = "DynamicCornerRadiusView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/ComponentFlow" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift new file mode 100644 index 00000000000..531a3bb4a83 --- /dev/null +++ b/submodules/TelegramUI/Components/DynamicCornerRadiusView/Sources/DynamicCornerRadiusView.swift @@ -0,0 +1,90 @@ +import Foundation +import UIKit +import ComponentFlow + +private func generatePath(size: CGSize, corners: DynamicCornerRadiusView.Corners) -> CGPath { + let path = CGMutablePath() + + var corners = corners + corners.minXMinY = max(0.01, corners.minXMinY) + corners.maxXMinY = max(0.01, corners.maxXMinY) + corners.minXMaxY = max(0.01, corners.minXMaxY) + corners.maxXMaxY = max(0.01, corners.maxXMaxY) + + path.move(to: CGPoint(x: 0.0, y: corners.minXMinY)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: corners.minXMinY, y: 0.0), radius: corners.minXMinY) + path.addLine(to: CGPoint(x: size.width - corners.maxXMinY, y: 0.0)) + path.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width, y: corners.maxXMinY), radius: corners.maxXMinY) + path.addLine(to: CGPoint(x: size.width, y: size.height - corners.maxXMaxY)) + path.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - corners.maxXMaxY, y: size.height), radius: corners.maxXMaxY) + path.addLine(to: CGPoint(x: corners.minXMaxY, y: size.height)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - corners.minXMaxY), radius: corners.minXMaxY) + path.closeSubpath() + + return path +} + +open class DynamicCornerRadiusView: UIView { + override public static var layerClass: AnyClass { + return CAShapeLayer.self + } + + public struct Corners: Equatable { + public var minXMinY: CGFloat + public var maxXMinY: CGFloat + public var minXMaxY: CGFloat + public var maxXMaxY: CGFloat + + public init(minXMinY: CGFloat, maxXMinY: CGFloat, minXMaxY: CGFloat, maxXMaxY: CGFloat) { + self.minXMinY = minXMinY + self.maxXMinY = maxXMinY + self.minXMaxY = minXMaxY + self.maxXMaxY = maxXMaxY + } + } + + private struct Params: Equatable { + var size: CGSize + var corners: Corners + + init(size: CGSize, corners: Corners) { + self.size = size + self.corners = corners + } + } + + private var params: Params? + + override public init(frame: CGRect) { + super.init(frame: frame) + + if let shapeLayer = self.layer as? CAShapeLayer { + shapeLayer.strokeColor = nil + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, corners: Corners, transition: Transition) { + let params = Params(size: size, corners: corners) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + public func updateColor(color: UIColor, transition: Transition) { + if let shapeLayer = self.layer as? CAShapeLayer { + transition.setShapeLayerFillColor(layer: shapeLayer, color: color) + } + } + + private func update(params: Params, transition: Transition) { + if let shapeLayer = self.layer as? CAShapeLayer { + transition.setShapeLayerPath(layer: shapeLayer, path: generatePath(size: params.size, corners: params.corners)) + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 8df436290e2..f2057cc3e84 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -64,6 +64,7 @@ public final class EmojiStatusSelectionComponent: Component { public let emojiContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor + public let color: UIColor? public let hideTopPanel: Bool public let disableTopPanel: Bool public let hideTopPanelUpdated: (Bool, Transition) -> Void @@ -73,6 +74,7 @@ public final class EmojiStatusSelectionComponent: Component { strings: PresentationStrings, deviceMetrics: DeviceMetrics, emojiContent: EmojiPagerContentComponent, + color: UIColor?, backgroundColor: UIColor, separatorColor: UIColor, hideTopPanel: Bool, @@ -83,6 +85,7 @@ public final class EmojiStatusSelectionComponent: Component { self.strings = strings self.deviceMetrics = deviceMetrics self.emojiContent = emojiContent + self.color = color self.backgroundColor = backgroundColor self.separatorColor = separatorColor self.hideTopPanel = hideTopPanel @@ -103,6 +106,9 @@ public final class EmojiStatusSelectionComponent: Component { if lhs.emojiContent != rhs.emojiContent { return false } + if lhs.color != rhs.color { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } @@ -169,7 +175,7 @@ public final class EmojiStatusSelectionComponent: Component { isContentInFocus: true, containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), - emojiContent: component.emojiContent, + emojiContent: component.emojiContent.withCustomTintColor(component.color), stickerContent: nil, maskContent: nil, gifContent: nil, @@ -200,7 +206,8 @@ public final class EmojiStatusSelectionComponent: Component { isExpanded: false, clipContentToTopPanel: false, useExternalSearchContainer: false, - hidePanels: component.disableTopPanel + hidePanels: component.disableTopPanel, + customTintColor: component.color )), environment: {}, containerSize: availableSize @@ -277,6 +284,7 @@ public final class EmojiStatusSelectionController: ViewController { private var validLayout: ContainerViewLayout? private let currentSelection: Int64? + private let color: UIColor? private var emojiContentDisposable: Disposable? private var emojiContent: EmojiPagerContentComponent? @@ -312,10 +320,11 @@ public final class EmojiStatusSelectionController: ViewController { private var isReactionSearchActive: Bool = false - init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal, currentSelection: Int64?) { + init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal, currentSelection: Int64?, color: UIColor?) { self.controller = controller self.context = context self.currentSelection = currentSelection + self.color = color if let sourceView = sourceView { self.globalSourceRect = sourceView.convert(sourceView.bounds, to: nil) @@ -826,17 +835,26 @@ public final class EmojiStatusSelectionController: ViewController { renderer: animationRenderer, placeholderColor: UIColor(white: 0.0, alpha: 0.0), pointSize: CGSize(width: 32.0, height: 32.0), - dynamicColor: self.presentationData.theme.list.itemAccentColor + dynamicColor: self.color ?? self.presentationData.theme.list.itemAccentColor ) - switch item.tintMode { - case let .custom(color): - baseItemLayer.contentTintColor = color - case .accent: - baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor - case .primary: - baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor - case .none: - break + if let color = self.color { + switch item.tintMode { + case .none: + break + default: + baseItemLayer.contentTintColor = color + } + } else { + switch item.tintMode { + case let .custom(color): + baseItemLayer.contentTintColor = color + case .accent: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemAccentColor + case .primary: + baseItemLayer.contentTintColor = self.presentationData.theme.list.itemPrimaryTextColor + case .none: + break + } } if let sublayers = animationLayer.sublayers { @@ -988,6 +1006,7 @@ public final class EmojiStatusSelectionController: ViewController { strings: self.presentationData.strings, deviceMetrics: layout.deviceMetrics, emojiContent: emojiContent, + color: self.color, backgroundColor: listBackgroundColor, separatorColor: separatorColor, hideTopPanel: self.isReactionSearchActive, @@ -1024,11 +1043,18 @@ public final class EmojiStatusSelectionController: ViewController { sourceOrigin = CGPoint(x: layout.size.width / 2.0, y: floor(layout.size.height / 2.0 - componentSize.height)) } + var componentFrame: CGRect + let pointsToTop: Bool if sourceOrigin.y + 5.0 + componentSize.height > layout.size.height - layout.insets(options: []).bottom { - sourceOrigin.y = layout.size.height - layout.insets(options: []).bottom - componentSize.height - 5.0 + componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y - 25.0 - componentSize.height), size: componentSize) + pointsToTop = false + } else { + componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize) + pointsToTop = true + } + if componentFrame.minY < layout.insets(options: [.statusBar]).top { + componentFrame.origin.y = layout.insets(options: [.statusBar]).top } - - let componentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - componentSize.width) / 2.0), y: sourceOrigin.y + 5.0), size: componentSize) if self.componentShadowLayer.bounds.size != componentFrame.size { let componentShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: componentFrame.size), cornerRadius: 24.0).cgPath @@ -1038,7 +1064,12 @@ public final class EmojiStatusSelectionController: ViewController { let cloudOffset0: CGFloat = 30.0 let cloudSize0: CGFloat = 16.0 - var cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + var cloudFrame0: CGRect + if pointsToTop { + cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + } else { + cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.maxY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0)) + } var invertX = false if cloudFrame0.maxX >= layout.size.width - layout.safeInsets.right - 32.0 { cloudFrame0.origin.x = floor(sourceOrigin.x - cloudSize0 - cloudOffset0 + cloudSize0 / 2.0) @@ -1055,7 +1086,12 @@ public final class EmojiStatusSelectionController: ViewController { let cloudOffset1 = CGPoint(x: -9.0, y: -14.0) let cloudSize1: CGFloat = 8.0 - var cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + var cloudFrame1: CGRect + if pointsToTop { + cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + } else { + cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY - cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1)) + } if invertX { cloudFrame1.origin.x = floor(cloudFrame0.midX - cloudSize1 - cloudOffset1.x + cloudSize1 / 2.0) } @@ -1076,7 +1112,7 @@ public final class EmojiStatusSelectionController: ViewController { let contentDuration: Double = 0.3 let contentDelay: Double = 0.14 - let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) + let initialContentFrame = CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)) if let emojiView = self.componentHost.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { emojiView.animateIn(fromLocation: self.view.convert(initialContentFrame.center, to: emojiView)) @@ -1084,7 +1120,7 @@ public final class EmojiStatusSelectionController: ViewController { componentView.layer.animatePosition(from: initialContentFrame.center, to: componentFrame.center, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateBounds(from: CGRect(origin: CGPoint(x: -(componentFrame.minX - initialContentFrame.minX), y: -(componentFrame.minY - initialContentFrame.minY)), size: initialContentFrame.size), to: CGRect(origin: CGPoint(), size: componentFrame.size), duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) - self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: componentFrame.minY), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) + self.componentShadowLayer.animateFrame(from: CGRect(origin: CGPoint(x: cloudFrame0.midX - 24.0, y: pointsToTop ? componentFrame.minY : (componentFrame.maxY - 24.0 * 2.0)), size: CGSize(width: 24.0 * 2.0, height: 24.0 * 2.0)), to: componentView.frame, duration: contentDuration, delay: contentDelay, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) self.componentShadowLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: contentDelay) @@ -1264,7 +1300,17 @@ public final class EmojiStatusSelectionController: ViewController { return } - if let _ = item, let destinationView = controller.destinationItemView() { + var animateOutToView = false + switch controller.mode { + case .backgroundSelection, .customStatusSelection, .quickReactionSelection: + if let itemFile = item?.itemFile, itemFile.fileId.id != 0 { + animateOutToView = true + } + case .statusSelection: + animateOutToView = true + } + + if animateOutToView, item != nil, let destinationView = controller.destinationItemView() { if let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { snapshotView.frame = destinationView.frame destinationView.superview?.insertSubview(snapshotView, belowSubview: destinationView) @@ -1279,6 +1325,10 @@ public final class EmojiStatusSelectionController: ViewController { case .statusSelection: let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile, expirationDate: nil) |> deliverOnMainQueue).start() + case let .backgroundSelection(completion): + completion(item?.itemFile) + case let .customStatusSelection(completion): + completion(item?.itemFile, nil) case let .quickReactionSelection(completion): if let item = item, let itemFile = item.itemFile { var selectedReaction: MessageReaction.Reaction? @@ -1304,7 +1354,7 @@ public final class EmojiStatusSelectionController: ViewController { completion() } - if let item = item, let destinationView = controller.destinationItemView() { + if animateOutToView, let item = item, let destinationView = controller.destinationItemView() { var emojiString: String? if let itemFile = item.itemFile { attributeLoop: for attribute in itemFile.attributes { @@ -1364,6 +1414,8 @@ public final class EmojiStatusSelectionController: ViewController { public enum Mode { case statusSelection + case backgroundSelection(completion: (TelegramMediaFile?) -> Void) + case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void) case quickReactionSelection(completion: () -> Void) } @@ -1371,6 +1423,7 @@ public final class EmojiStatusSelectionController: ViewController { private weak var sourceView: UIView? private let emojiContent: Signal private let currentSelection: Int64? + private let color: UIColor? private let mode: Mode private let destinationItemView: () -> UIView? @@ -1383,12 +1436,13 @@ public final class EmojiStatusSelectionController: ViewController { return true } - public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, destinationItemView: @escaping () -> UIView?) { + public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, color: UIColor? = nil, destinationItemView: @escaping () -> UIView?) { self.context = context self.mode = mode self.sourceView = sourceView self.emojiContent = emojiContent self.currentSelection = currentSelection + self.color = color self.destinationItemView = destinationItemView super.init(navigationBarPresentationData: nil) @@ -1418,7 +1472,7 @@ public final class EmojiStatusSelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection) + self.displayNode = Node(controller: self, context: self.context, sourceView: self.sourceView, emojiContent: self.emojiContent, currentSelection: self.currentSelection, color: self.color) super.displayNodeDidLoad() } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index f4f10117086..2674eb490d9 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -144,6 +144,58 @@ public func animationCacheFetchFile(postbox: Postbox, userLocation: MediaResourc } } +private func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? { + return generateImage(bounds, rotatedContext: { contextSize, context in + let bounds = CGRect(origin: CGPoint(), size: contextSize) + context.clear(bounds) + + let circleBounds = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - size.width) / 2.0), y: floorToScreenPixels((bounds.height - size.height) / 2.0)), size: size) + context.addEllipse(in: circleBounds) + context.clip() + + if let secondColor = nameColor.secondary { + var firstColor = nameColor.main + var secondColor = secondColor + if isDark, nameColor.tertiary == nil { + firstColor = secondColor + secondColor = nameColor.main + } + + context.setFillColor(secondColor.cgColor) + context.fill(circleBounds) + + if let thirdColor = nameColor.tertiary { + context.move(to: CGPoint(x: contextSize.width, y: 0.0)) + context.addLine(to: CGPoint(x: contextSize.width, y: contextSize.height)) + context.addLine(to: CGPoint(x: 0.0, y: contextSize.height)) + context.closePath() + context.setFillColor(firstColor.cgColor) + context.fillPath() + + context.setFillColor(thirdColor.cgColor) + context.translateBy(x: contextSize.width / 2.0, y: contextSize.height / 2.0) + context.rotate(by: .pi / 4.0) + + let rectSide = size.width / 40.0 * 18.0 + let rectCornerRadius = round(size.width / 40.0 * 4.0) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: -rectSide / 2.0, y: -rectSide / 2.0), size: CGSize(width: rectSide, height: rectSide)), cornerRadius: rectCornerRadius) + context.addPath(path.cgPath) + context.fillPath() + } else { + context.move(to: .zero) + context.addLine(to: CGPoint(x: contextSize.width, y: 0.0)) + context.addLine(to: CGPoint(x: 0.0, y: contextSize.height)) + context.closePath() + context.setFillColor(firstColor.cgColor) + context.fillPath() + } + } else { + context.setFillColor(nameColor.main.cgColor) + context.fill(circleBounds) + } + }) +} + public final class InlineStickerItemLayer: MultiAnimationRenderTarget { public enum Context: Equatable { public final class Custom: Equatable { @@ -317,8 +369,13 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { super.init() - if let topicInfo = emoji.topicInfo { - self.updateTopicInfo(topicInfo: topicInfo) + if let custom = emoji.custom { + switch custom { + case let .topic(id, info): + self.updateTopicInfo(topicInfo: (id, info)) + case let .nameColors(colors): + self.updateNameColors(colors: colors) + } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) } else { @@ -467,6 +524,30 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + private func updateNameColors(colors: [UInt32]) { + if colors.isEmpty { + return + } + let main = UIColor(rgb: colors[0]) + var secondary: UIColor? + if colors.count >= 2 { + secondary = UIColor(rgb: colors[1]) + } + var tertiary: UIColor? + if colors.count >= 3 { + tertiary = UIColor(rgb: colors[2]) + } + let mappedColor = PeerNameColors.Colors(main: main, secondary: secondary, tertiary: tertiary) + + let image = generateImage(CGSize(width: 18.0, height: 18.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = generatePeerNameColorImage(nameColor: mappedColor, isDark: false, bounds: CGSize(width: 18.0, height: 18.0), size: CGSize(width: 16.0, height: 16.0))?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size)) + } + }) + self.contents = image?.cgImage + } + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { if self.file?.fileId == file.fileId { return diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 2daaa91ec34..a08c0ef49f8 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -1,9 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -NGDEPS = [ - "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", -] - swift_library( name = "EntityKeyboard", module_name = "EntityKeyboard", @@ -13,7 +9,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = NGDEPS + [ + deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 7573cf2a5c8..ffd8d3fb038 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1,6 +1,3 @@ -// MARK: Nicegram StickerMaker -import FeatPartners -// import Foundation import UIKit import Display @@ -1614,12 +1611,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private var textFrame: CGRect? private var textField: EmojiSearchTextField? - // MARK: Nicegram StickerMaker - let stickerMakerButton = StickerMakerButton( - location: .stickerSearch - ) - // - private var tapRecognizer: UITapGestureRecognizer? private(set) var currentPresetSearchTerm: [String]? @@ -1652,10 +1643,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { super.init(frame: CGRect()) - // MARK: Nicegram StickerMaker - self.addSubview(stickerMakerButton) - // - self.layer.addSublayer(self.backgroundLayer) self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer) @@ -2148,22 +2135,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { } let _ = hasText - // MARK: Nicegram StickerMaker - transition.setFrame( - view: stickerMakerButton, - frame: CGRect( - origin: CGPoint( - x: backgroundFrame.minX, - y: backgroundFrame.maxY + StickerMakerButton.searchStickersConfig.topPadding - ), - size: CGSize( - width: backgroundFrame.width, - height: StickerMakerButton.height - ) - ) - ) - // - /*self.tintTextView.view?.isHidden = hasText self.textView.view?.isHidden = hasText*/ } @@ -2705,12 +2676,6 @@ public final class EmojiPagerContentComponent: Component { } } - // MARK: Nicegram StickerMaker - public var isStickersTab: Bool { - id == AnyHashable("stickers") - } - // - public let id: AnyHashable public let context: AccountContext public let avatarPeer: EnginePeer? @@ -3002,9 +2967,6 @@ public final class EmojiPagerContentComponent: Component { var premiumButtonHeight: CGFloat init( - // MARK: Nicegram StickerMaker - showStickerMakerButton: Bool, - // layoutType: ItemLayoutType, width: CGFloat, containerInsets: UIEdgeInsets, @@ -3026,13 +2988,6 @@ public final class EmojiPagerContentComponent: Component { self.searchHeight = 54.0 self.searchInsets = UIEdgeInsets(top: max(0.0, containerInsets.top - 8.0), left: containerInsets.left, bottom: 0.0, right: containerInsets.right) - // MARK: Nicegram StickerMaker - if showStickerMakerButton { - let additionalHeight = StickerMakerButton.height + StickerMakerButton.searchStickersConfig.topPadding - self.searchHeight += additionalHeight - } - // - self.curveNearBounds = curveNearBounds let minItemsPerRow: Int @@ -6490,11 +6445,6 @@ public final class EmojiPagerContentComponent: Component { self.component = component self.state = state - // MARK: Nicegram StickerMaker - let showStickerMaker = component.isStickersTab && StickerMaker.showConfig.stickerSearch - self.visibleSearchHeader?.stickerMakerButton.isHidden = !showStickerMaker - // - if component.searchAlwaysActive { self.isSearchActivated = true } @@ -6740,23 +6690,7 @@ public final class EmojiPagerContentComponent: Component { } let availableCustomContentSize = availableSize let customContentViewSize = customContentView.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableCustomContentSize, transition: customContentViewTransition) - - // MARK: Nicegram StickerMaker - var searchHeight: CGFloat - if component.displaySearchWithPlaceholder != nil { - searchHeight = 54 - - if showStickerMaker { - let additionalHeight = StickerMakerButton.height + StickerMakerButton.searchStickersConfig.topPadding - searchHeight += additionalHeight - } - } else { - searchHeight = 0 - } - // - - // MARK: Nicegram StickerMaker, use searchHeight for 'y' calculation - customContentViewTransition.setFrame(view: customContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top + searchHeight), size: customContentViewSize)) + customContentViewTransition.setFrame(view: customContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top + (component.displaySearchWithPlaceholder != nil ? 54.0 : 0.0)), size: customContentViewSize)) customContentHeight = customContentViewSize.height } else { @@ -6791,9 +6725,6 @@ public final class EmojiPagerContentComponent: Component { } let extractedExpr = ItemLayout( - // MARK: Nicegram StickerMaker - showStickerMakerButton: showStickerMaker, - // layoutType: component.itemLayoutType, width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right), @@ -7257,6 +7188,7 @@ public final class EmojiPagerContentComponent: Component { public enum Subject: Equatable { case generic case status + case channelStatus case reaction(onlyTop: Bool) case emoji case topicIcon @@ -7308,6 +7240,19 @@ public final class EmojiPagerContentComponent: Component { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) + iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) + |> map { result -> [TelegramMediaFile] in + switch result { + case let .result(_, items, _): + return items.map(\.file) + default: + return [] + } + } + |> take(1) + } else if case .channelStatus = subject { + orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) + iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { @@ -7352,6 +7297,8 @@ public final class EmojiPagerContentComponent: Component { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) } else if case .status = subject { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) + } else if case .channelStatus = subject { + searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) } else if [.profilePhoto, .groupPhoto].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) } else { @@ -7633,6 +7580,166 @@ public final class EmojiPagerContentComponent: Component { } } + if let recentStatusEmoji = recentStatusEmoji { + for item in recentStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + break + } + + itemGroups[groupIndex].items.append(resultItem) + } + } + } + if let featuredStatusEmoji = featuredStatusEmoji { + for item in featuredStatusEmoji.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + + let file = item.media + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + let resultItem: EmojiPagerContentComponent.Item + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { + break + } + + itemGroups[groupIndex].items.append(resultItem) + } + } + } + } else if case .channelStatus = subject { + let resultItem = EmojiPagerContentComponent.Item( + animationData: nil, + content: .icon(.stop), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .accent + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) + } + + var existingIds = Set() + + for file in iconStatusEmoji.prefix(7) { + if existingIds.contains(file.fileId) { + continue + } + existingIds.insert(file.fileId) + + var tintMode: Item.TintMode = .none + if file.isCustomTemplateEmoji { + tintMode = .accent + } + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute { + switch packReference { + case let .id(id, _): + if id == 773947703670341676 || id == 2964141614563343 { + tintMode = .accent + } + default: + break + } + } + } + + let resultItem: EmojiPagerContentComponent.Item + + let animationData = EntityKeyboardAnimationData(file: file) + resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } + } + if let recentStatusEmoji = recentStatusEmoji { for item in recentStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { @@ -8212,7 +8319,7 @@ public final class EmojiPagerContentComponent: Component { var isTemplate = false var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if [.status, .backgroundIcon].contains(subject) { + if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { @@ -8298,7 +8405,7 @@ public final class EmojiPagerContentComponent: Component { for item in featuredEmojiPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { - if [.status, .backgroundIcon].contains(subject) { + if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { @@ -8430,8 +8537,8 @@ public final class EmojiPagerContentComponent: Component { ) } - let warpContentsOnEdges = [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .profilePhoto, .groupPhoto, .backgroundIcon].contains(subject) - let enableLongPress = [.reaction(onlyTop: true), .reaction(onlyTop: false), .status].contains(subject) + let warpContentsOnEdges = [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .channelStatus, .profilePhoto, .groupPhoto, .backgroundIcon].contains(subject) + let enableLongPress = [.reaction(onlyTop: true), .reaction(onlyTop: false), .status, .channelStatus].contains(subject) return EmojiPagerContentComponent( id: "emoji", diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 1cd12e7d6a7..2df3092fd4a 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -70,7 +70,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { public var timerUpdated: ((NSNumber?) -> Void)? public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) } public func caption() -> NSAttributedString { diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD new file mode 100644 index 00000000000..dae202b12d6 --- /dev/null +++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListActionItemComponent", + module_name = "ListActionItemComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/ListSectionComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift new file mode 100644 index 00000000000..fb5467ad0a1 --- /dev/null +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -0,0 +1,187 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import ListSectionComponent + +public final class ListActionItemComponent: Component { + public let theme: PresentationTheme + public let title: AnyComponent + public let icon: AnyComponentWithIdentity? + public let hasArrow: Bool + public let action: ((UIView) -> Void)? + + public init( + theme: PresentationTheme, + title: AnyComponent, + icon: AnyComponentWithIdentity?, + hasArrow: Bool = true, + action: ((UIView) -> Void)? + ) { + self.theme = theme + self.title = title + self.icon = icon + self.hasArrow = hasArrow + self.action = action + } + + public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.hasArrow != rhs.hasArrow { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private let title = ComponentView() + private var icon: ComponentView? + + private let arrowView: UIImageView + + private var component: ListActionItemComponent? + + public var iconView: UIView? { + return self.icon?.view + } + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + + public override init(frame: CGRect) { + self.arrowView = UIImageView() + + super.init(frame: CGRect()) + + self.addSubview(self.arrowView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.internalHighligthedChanged = { [weak self] isHighlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(isHighlighted) + } + } + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func pressed() { + self.component?.action?(self) + } + + func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component + + self.isEnabled = component.action != nil + + let verticalInset: CGFloat = 11.0 + + let contentLeftInset: CGFloat = 16.0 + let contentRightInset: CGFloat = component.hasArrow ? 30.0 : 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += verticalInset + + let titleSize = self.title.update( + transition: transition, + component: component.title, + environment: {}, + containerSize: CGSize(width: availableSize.width - contentLeftInset, height: availableSize.height) + ) + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + contentHeight += titleSize.height + + contentHeight += verticalInset + + if let iconValue = component.icon { + if previousComponent?.icon?.id != iconValue.id, let icon = self.icon { + self.icon = nil + if let iconView = icon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + + var iconTransition = transition + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + iconTransition = iconTransition.withAnimation(.none) + icon = ComponentView() + self.icon = icon + } + + let iconSize = icon.update( + transition: iconTransition, + component: iconValue.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.addSubview(iconView) + transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + } + iconTransition.setFrame(view: iconView, frame: iconFrame) + } + } else { + if let icon = self.icon { + self.icon = nil + if let iconView = icon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + } + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate) + } + self.arrowView.tintColor = component.theme.list.disclosureArrowColor + if let image = self.arrowView.image { + let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) + transition.setFrame(view: self.arrowView, frame: arrowFrame) + } + transition.setAlpha(view: self.arrowView, alpha: component.hasArrow ? 1.0 : 0.0) + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD b/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD new file mode 100644 index 00000000000..1ebd9ea2ec5 --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListItemComponentAdaptor", + module_name = "ListItemComponentAdaptor", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift new file mode 100644 index 00000000000..ba4e12d70e7 --- /dev/null +++ b/submodules/TelegramUI/Components/ListItemComponentAdaptor/Sources/ListItemComponentAdaptor.swift @@ -0,0 +1,131 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import ComponentDisplayAdapters + +public protocol _ListItemComponentAdaptorItemGenerator: AnyObject, Equatable { + func item() -> ListViewItem +} + +public final class ListItemComponentAdaptor: Component { + public typealias ItemGenerator = _ListItemComponentAdaptorItemGenerator + + private let itemGenerator: AnyObject + private let isEqualImpl: (AnyObject) -> Bool + private let itemImpl: () -> ListViewItem + private let params: ListViewItemLayoutParams + + public init( + itemGenerator: ItemGeneratorType, + params: ListViewItemLayoutParams + ) { + self.itemGenerator = itemGenerator + self.isEqualImpl = { other in + if let other = other as? ItemGeneratorType, itemGenerator == other { + return true + } else { + return false + } + } + self.itemImpl = { + return itemGenerator.item() + } + self.params = params + } + + public static func ==(lhs: ListItemComponentAdaptor, rhs: ListItemComponentAdaptor) -> Bool { + if !lhs.isEqualImpl(rhs.itemGenerator) { + return false + } + if lhs.params != rhs.params { + return false + } + return true + } + + public final class View: UIView { + private var itemNode: ListViewItemNode? + + func update(component: ListItemComponentAdaptor, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let item = component.itemImpl() + + if let itemNode = self.itemNode { + let mappedAnimation: ListViewItemUpdateAnimation + switch transition.animation { + case .none: + mappedAnimation = .None + case let .curve(duration, curve): + mappedAnimation = .System(duration: duration, transition: ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false)) + } + + var resultSize: CGSize? + item.updateNode( + async: { f in f() }, + node: { return itemNode }, + params: component.params, + previousItem: nil, + nextItem: nil, + animation: mappedAnimation, + completion: { [weak itemNode] layout, apply in + resultSize = layout.size + + guard let itemNode else { + return + } + + let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + } + ) + if let resultSize { + transition.setFrame(view: itemNode.view, frame: CGRect(origin: CGPoint(), size: resultSize)) + return resultSize + } else { + #if DEBUG + assertionFailure() + #endif + return self.bounds.size + } + } else { + var itemNode: ListViewItemNode? + item.nodeConfiguredForParams( + async: { f in f() }, + params: component.params, + synchronousLoads: true, + previousItem: nil, + nextItem: nil, + completion: { result, apply in + itemNode = result + apply().1(ListViewItemApply(isOnScreen: true)) + } + ) + if let itemNode { + self.itemNode = itemNode + self.addSubnode(itemNode) + + return itemNode.bounds.size + } else { + #if DEBUG + assertionFailure() + #endif + return self.bounds.size + } + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListSectionComponent/BUILD b/submodules/TelegramUI/Components/ListSectionComponent/BUILD new file mode 100644 index 00000000000..e216e0bb5df --- /dev/null +++ b/submodules/TelegramUI/Components/ListSectionComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ListSectionComponent", + module_name = "ListSectionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift new file mode 100644 index 00000000000..5612d0f692a --- /dev/null +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -0,0 +1,288 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import DynamicCornerRadiusView + +public protocol ListSectionComponentChildView: AnyObject { + var customUpdateIsHighlighted: ((Bool) -> Void)? { get set } +} + +public final class ListSectionComponent: Component { + public typealias ChildView = ListSectionComponentChildView + + public enum Background: Equatable { + case none + case all + case range(from: AnyHashable, corners: DynamicCornerRadiusView.Corners) + } + + public let theme: PresentationTheme + public let background: Background + public let header: AnyComponent? + public let footer: AnyComponent? + public let items: [AnyComponentWithIdentity] + + public init( + theme: PresentationTheme, + background: Background = .all, + header: AnyComponent?, + footer: AnyComponent?, + items: [AnyComponentWithIdentity] + ) { + self.theme = theme + self.background = background + self.header = header + self.footer = footer + self.items = items + } + + public static func ==(lhs: ListSectionComponent, rhs: ListSectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.background != rhs.background { + return false + } + if lhs.header != rhs.header { + return false + } + if lhs.footer != rhs.footer { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + public final class View: UIView { + private let contentView: UIView + private let contentBackgroundView: DynamicCornerRadiusView + + private var header: ComponentView? + private var footer: ComponentView? + private var itemViews: [AnyHashable: ComponentView] = [:] + + private var isHighlighted: Bool = false + + private var component: ListSectionComponent? + + public override init(frame: CGRect) { + self.contentView = UIView() + self.contentView.layer.cornerRadius = 11.0 + self.contentView.clipsToBounds = true + + self.contentBackgroundView = DynamicCornerRadiusView() + + super.init(frame: CGRect()) + + self.addSubview(self.contentBackgroundView) + self.addSubview(self.contentView) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + private func updateIsHighlighted(isHighlighted: Bool) { + if self.isHighlighted == isHighlighted { + return + } + self.isHighlighted = isHighlighted + + guard let component = self.component else { + return + } + + let transition: Transition + let backgroundColor: UIColor + if isHighlighted { + transition = .immediate + backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + transition = .easeInOut(duration: 0.2) + backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) + } + + func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let backgroundColor: UIColor + if self.isHighlighted { + backgroundColor = component.theme.list.itemHighlightedBackgroundColor + } else { + backgroundColor = component.theme.list.itemBlocksBackgroundColor + } + self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) + + let headerSideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 0.0 + + if let headerValue = component.header { + let header: ComponentView + var headerTransition = transition + if let current = self.header { + header = current + } else { + headerTransition = headerTransition.withAnimation(.none) + header = ComponentView() + self.header = header + } + + let headerSize = header.update( + transition: headerTransition, + component: headerValue, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height) + ) + if let headerView = header.view { + if headerView.superview == nil { + self.addSubview(headerView) + } + headerTransition.setFrame(view: headerView, frame: CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: headerSize)) + } + contentHeight += headerSize.height + } else { + if let header = self.header { + self.header = nil + header.view?.removeFromSuperview() + } + } + + var innerContentHeight: CGFloat = 0.0 + var validItemIds: [AnyHashable] = [] + for item in component.items { + validItemIds.append(item.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.itemViews[item.id] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ComponentView() + self.itemViews[item.id] = itemView + } + + let itemSize = itemView.update( + transition: itemTransition, + component: item.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.contentView.addSubview(itemComponentView) + transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0) + + if let itemComponentView = itemComponentView as? ChildView { + itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in + guard let self else { + return + } + self.updateIsHighlighted(isHighlighted: isHighlighted) + } + } + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + innerContentHeight += itemSize.height + } + var removedItemIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } + } + for id in removedItemIds { + self.itemViews.removeValue(forKey: id) + } + + if innerContentHeight != 0.0 && contentHeight != 0.0 { + contentHeight += 7.0 + } + + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: innerContentHeight)) + transition.setFrame(view: self.contentView, frame: contentFrame) + + let backgroundFrame: CGRect + var backgroundAlpha: CGFloat = 1.0 + switch component.background { + case .none: + backgroundFrame = contentFrame + backgroundAlpha = 0.0 + self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + case .all: + backgroundFrame = contentFrame + self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + case let .range(from, corners): + if let itemComponentView = self.itemViews[from]?.view, itemComponentView.frame.minY < contentFrame.height { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemComponentView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemComponentView.frame.minY)) + } else { + backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minY, y: contentFrame.height), size: CGSize(width: contentFrame.width, height: 0.0)) + } + self.contentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) + } + transition.setFrame(view: self.contentBackgroundView, frame: backgroundFrame) + transition.setAlpha(view: self.contentBackgroundView, alpha: backgroundAlpha) + + contentHeight += innerContentHeight + + if let footerValue = component.footer { + let footer: ComponentView + var footerTransition = transition + if let current = self.footer { + footer = current + } else { + footerTransition = footerTransition.withAnimation(.none) + footer = ComponentView() + self.footer = footer + } + + let footerSize = footer.update( + transition: footerTransition, + component: footerValue, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: availableSize.height) + ) + if contentHeight != 0.0 { + contentHeight += 7.0 + } + if let footerView = footer.view { + if footerView.superview == nil { + self.addSubview(footerView) + } + footerTransition.setFrame(view: footerView, frame: CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: footerSize)) + } + contentHeight += footerSize.height + } else { + if let footer = self.footer { + self.footer = nil + footer.view?.removeFromSuperview() + } + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/BUILD b/submodules/TelegramUI/Components/MediaEditor/BUILD index 5ee753895f7..54f2c6a7b93 100644 --- a/submodules/TelegramUI/Components/MediaEditor/BUILD +++ b/submodules/TelegramUI/Components/MediaEditor/BUILD @@ -68,6 +68,7 @@ swift_library( "//submodules/StickerResources:StickerResources", "//submodules/YuvConversion:YuvConversion", "//submodules/FastBlur:FastBlur", + "//submodules/WallpaperBackgroundNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 238f03137bb..72910fd8db8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -62,20 +62,31 @@ public enum CodableDrawingEntity: Equatable { private var coordinates: MediaArea.Coordinates? { var position: CGPoint? var size: CGSize? - var scale: CGFloat? var rotation: CGFloat? + var scale: CGFloat? switch self { case let .location(entity): position = entity.position size = entity.renderImage?.size - scale = entity.scale rotation = entity.rotation - case let .sticker(entity): - position = entity.position - size = entity.baseSize scale = entity.scale - rotation = entity.rotation + case let .sticker(entity): + var entityPosition = entity.position + var entitySize = entity.baseSize + let entityRotation = entity.rotation + let entityScale = entity.scale + + if case .message = entity.content { + let offset: CGFloat = 16.18 * entityScale //54.0 * entityScale / 3.337 + entitySize = CGSize(width: entitySize.width - 38.0, height: entitySize.height - 4.0) + entityPosition = CGPoint(x: entityPosition.x + offset * cos(entityRotation), y: entityPosition.y + offset * sin(entityRotation)) + } + + position = entityPosition + size = entitySize + rotation = entityRotation + scale = entityScale default: return nil } @@ -123,6 +134,8 @@ public enum CodableDrawingEntity: Equatable { reaction: reaction, flags: flags ) + } else if case let .message(messageIds, _, _) = entity.content, let messageId = messageIds.first { + return .channelMessage(coordinates: coordinates, messageId: messageId) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift index 1b90f2c0f55..ffaa20618b5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift @@ -7,52 +7,52 @@ import AccountContext import Photos public final class DrawingMediaEntity: DrawingEntity, Codable { - public enum Content: Equatable { - case image(UIImage, PixelDimensions) - case video(String, PixelDimensions) - case asset(PHAsset) - - var dimensions: PixelDimensions { - switch self { - case let .image(_, dimensions), let .video(_, dimensions): - return dimensions - case let .asset(asset): - return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) - } - } - - public static func == (lhs: Content, rhs: Content) -> Bool { - switch lhs { - case let .image(lhsImage, lhsDimensions): - if case let .image(rhsImage, rhsDimensions) = rhs { - return lhsImage === rhsImage && lhsDimensions == rhsDimensions - } else { - return false - } - case let .video(lhsPath, lhsDimensions): - if case let .video(rhsPath, rhsDimensions) = rhs { - return lhsPath == rhsPath && lhsDimensions == rhsDimensions - } else { - return false - } - case let .asset(lhsAsset): - if case let .asset(rhsAsset) = rhs { - return lhsAsset.localIdentifier == rhsAsset.localIdentifier - } else { - return false - } - } - } - } +// public enum Content: Equatable { +// case image(UIImage, PixelDimensions) +// case video(String, PixelDimensions) +// case asset(PHAsset) +// +// var dimensions: PixelDimensions { +// switch self { +// case let .image(_, dimensions), let .video(_, dimensions): +// return dimensions +// case let .asset(asset): +// return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) +// } +// } +// +// public static func == (lhs: Content, rhs: Content) -> Bool { +// switch lhs { +// case let .image(lhsImage, lhsDimensions): +// if case let .image(rhsImage, rhsDimensions) = rhs { +// return lhsImage === rhsImage && lhsDimensions == rhsDimensions +// } else { +// return false +// } +// case let .video(lhsPath, lhsDimensions): +// if case let .video(rhsPath, rhsDimensions) = rhs { +// return lhsPath == rhsPath && lhsDimensions == rhsDimensions +// } else { +// return false +// } +// case let .asset(lhsAsset): +// if case let .asset(rhsAsset) = rhs { +// return lhsAsset.localIdentifier == rhsAsset.localIdentifier +// } else { +// return false +// } +// } +// } +// } private enum CodingKeys: String, CodingKey { case uuid - case image - case videoPath - case assetId +// case image +// case videoPath +// case assetId case size - case width - case height +// case width +// case height case referenceDrawingSize case position case scale @@ -61,7 +61,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { } public var uuid: UUID - public let content: Content +// public let content: Content public let size: CGSize public var referenceDrawingSize: CGSize @@ -82,14 +82,15 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { } public var isAnimated: Bool { - switch self.content { - case .image: - return false - case .video: - return true - case let .asset(asset): - return asset.mediaType == .video - } + return false +// switch self.content { +// case .image: +// return false +// case .video: +// return true +// case let .asset(asset): +// return asset.mediaType == .video +// } } public var isMedia: Bool { @@ -99,9 +100,9 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { public var renderImage: UIImage? public var renderSubEntities: [DrawingEntity]? - public init(content: Content, size: CGSize) { + public init(size: CGSize) { self.uuid = UUID() - self.content = content +// self.content = content self.size = size self.referenceDrawingSize = .zero @@ -115,18 +116,18 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) self.size = try container.decode(CGSize.self, forKey: .size) - let width = try container.decode(Int32.self, forKey: .width) - let height = try container.decode(Int32.self, forKey: .height) - if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { - self.content = .video(videoPath, PixelDimensions(width: width, height: height)) - } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { - self.content = .image(image, PixelDimensions(width: width, height: height)) - } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { - fatalError() - //self.content = .asset() - } else { - fatalError() - } +// let width = try container.decode(Int32.self, forKey: .width) +// let height = try container.decode(Int32.self, forKey: .height) +// if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { +// self.content = .video(videoPath, PixelDimensions(width: width, height: height)) +// } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { +// self.content = .image(image, PixelDimensions(width: width, height: height)) +// } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { +// fatalError() +// //self.content = .asset() +// } else { +// fatalError() +// } self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) self.position = try container.decode(CGPoint.self, forKey: .position) self.scale = try container.decode(CGFloat.self, forKey: .scale) @@ -137,18 +138,18 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.uuid, forKey: .uuid) - switch self.content { - case let .video(videoPath, dimensions): - try container.encode(videoPath, forKey: .videoPath) - try container.encode(dimensions.width, forKey: .width) - try container.encode(dimensions.height, forKey: .height) - case let .image(image, dimensions): - try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) - try container.encode(dimensions.width, forKey: .width) - try container.encode(dimensions.height, forKey: .height) - case let .asset(asset): - try container.encode(asset.localIdentifier, forKey: .assetId) - } +// switch self.content { +// case let .video(videoPath, dimensions): +// try container.encode(videoPath, forKey: .videoPath) +// try container.encode(dimensions.width, forKey: .width) +// try container.encode(dimensions.height, forKey: .height) +// case let .image(image, dimensions): +// try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) +// try container.encode(dimensions.width, forKey: .width) +// try container.encode(dimensions.height, forKey: .height) +// case let .asset(asset): +// try container.encode(asset.localIdentifier, forKey: .assetId) +// } try container.encode(self.size, forKey: .size) try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) @@ -158,7 +159,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { } public func duplicate(copy: Bool) -> DrawingEntity { - let newEntity = DrawingMediaEntity(content: self.content, size: self.size) + let newEntity = DrawingMediaEntity(size: self.size) newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.scale = self.scale @@ -174,9 +175,9 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { if self.uuid != other.uuid { return false } - if self.content != other.content { - return false - } +// if self.content != other.content { +// return false +// } if self.size != other.size { return false } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 41e3fda58ae..3f7b805d3b1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import AccountContext +import Postbox import TelegramCore private func entitiesPath() -> String { @@ -29,8 +30,10 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } case file(TelegramMediaFile, FileType) case image(UIImage, ImageType) + case animatedImage(Data, UIImage) case video(TelegramMediaFile) case dualVideoReference(Bool) + case message([MessageId], TelegramMediaFile?, CGSize) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -46,6 +49,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } + case let .animatedImage(lhsData, lhsThumbnailImage): + if case let .animatedImage(rhsData, rhsThumbnailImage) = lhs { + return lhsData == rhsData && lhsThumbnailImage === rhsThumbnailImage + } else { + return false + } case let .video(lhsFile): if case let .video(rhsFile) = rhs { return lhsFile.fileId == rhsFile.fileId @@ -58,6 +67,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } + case let .message(messageIds, innerFile, size): + if case .message(messageIds, innerFile, size) = rhs { + return true + } else { + return false + } } } } @@ -67,11 +82,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case reaction case reactionStyle case imagePath + case animatedImagePath case videoFile case isRectangle case isDualPhoto case dualVideo case isAdditionalVideo + case messageIds + case explicitSize case referenceDrawingSize case position case scale @@ -90,6 +108,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { didSet { if case let .file(_, type) = self.content, case .reaction = type { self.scale = max(0.59, min(1.77, self.scale)) + } else if case .message = self.content { + self.scale = max(2.5, self.scale) } } } @@ -101,6 +121,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var color: DrawingColor = DrawingColor.clear public var lineWidth: CGFloat = 0.0 + public var secondaryRenderImage: UIImage? + public var center: CGPoint { return self.position } @@ -112,6 +134,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { switch self.content { case let .image(image, _): dimensions = image.size + case let .animatedImage(_, thumbnailImage): + dimensions = thumbnailImage.size case let .file(file, type): if case .reaction = type { dimensions = CGSize(width: 512.0, height: 512.0) @@ -122,6 +146,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: dimensions = CGSize(width: 512.0, height: 512.0) + case let .message(_, _, size): + dimensions = size } let boundingSize = CGSize(width: size, height: size) @@ -143,10 +169,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } case .image: return false + case .animatedImage: + return true case .video: return true case .dualVideoReference: return true + case .message: + return !(self.renderSubEntities ?? []).isEmpty } } @@ -156,6 +186,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return imageType == .rectangle case .video: return true + case .message: + return true default: return false } @@ -184,7 +216,10 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) - if let _ = try container.decodeIfPresent(Bool.self, forKey: .dualVideo) { + if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { + let size = try container.decodeIfPresent(CGSize.self, forKey: .explicitSize) ?? .zero + self.content = .message(messageIds, nil, size) + } else if let _ = try container.decodeIfPresent(Bool.self, forKey: .dualVideo) { let isAdditional = try container.decodeIfPresent(Bool.self, forKey: .isAdditionalVideo) ?? false self.content = .dualVideoReference(isAdditional) } else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) { @@ -211,6 +246,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { imageType = .sticker } self.content = .image(image, imageType) + } else if let dataPath = try container.decodeIfPresent(String.self, forKey: .animatedImagePath), let data = try? Data(contentsOf: URL(fileURLWithPath: fullEntityMediaPath(dataPath))), let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let thumbnailImage = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { + self.content = .animatedImage(data, thumbnailImage) } else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) { self.content = .video(file) } else { @@ -257,11 +294,29 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { default: break } + case let .animatedImage(data, thumbnailImage): + let dataPath = "\(self.uuid).heics" + let fullDataPath = fullEntityMediaPath(dataPath) + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? data.write(to: URL(fileURLWithPath: fullDataPath)) + try container.encodeIfPresent(dataPath, forKey: .animatedImagePath) + + let imagePath = "\(self.uuid).png" + let fullImagePath = fullEntityMediaPath(imagePath) + if let imageData = thumbnailImage.pngData() { + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) + try container.encodeIfPresent(imagePath, forKey: .imagePath) + } case let .video(file): try container.encode(file, forKey: .videoFile) case let .dualVideoReference(isAdditional): try container.encode(true, forKey: .dualVideo) try container.encode(isAdditional, forKey: .isAdditionalVideo) + case let .message(messageIds, innerFile, size): + try container.encode(messageIds, forKey: .messageIds) + let _ = innerFile + try container.encode(size, forKey: .explicitSize) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift new file mode 100644 index 00000000000..c39d2d363c3 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -0,0 +1,297 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import AccountContext +import WallpaperBackgroundNode + +public final class DrawingWallpaperRenderer { + private let context: AccountContext + private let dayWallpaper: TelegramWallpaper? + private let nightWallpaper: TelegramWallpaper? + + private let wallpaperBackgroundNode: WallpaperBackgroundNode + private let darkWallpaperBackgroundNode: WallpaperBackgroundNode + + public init (context: AccountContext, dayWallpaper: TelegramWallpaper?, nightWallpaper: TelegramWallpaper?) { + self.context = context + self.dayWallpaper = dayWallpaper + self.nightWallpaper = nightWallpaper + + self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false) + self.wallpaperBackgroundNode.displaysAsynchronously = false + + let wallpaper = self.dayWallpaper ?? context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper + self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false) + + self.darkWallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false) + self.darkWallpaperBackgroundNode.displaysAsynchronously = false + + let darkTheme = defaultDarkColorPresentationTheme + let darkWallpaper = self.nightWallpaper ?? darkTheme.chat.defaultWallpaper + self.darkWallpaperBackgroundNode.update(wallpaper: darkWallpaper, animated: false) + } + + public func render(completion: @escaping (CGSize, UIImage?, UIImage?, CGRect?) -> Void) { + self.updateLayout(size: CGSize(width: 360.0, height: 640.0)) + + let resultSize = CGSize(width: 1080, height: 1920) + Queue.mainQueue().justDispatch { + self.generate(view: self.wallpaperBackgroundNode.view) { dayImage in + if self.dayWallpaper != nil && self.nightWallpaper == nil { + completion(resultSize, dayImage, nil, nil) + } else { + Queue.mainQueue().justDispatch { + self.generate(view: self.darkWallpaperBackgroundNode.view) { nightImage in + completion(resultSize, dayImage, nightImage, nil) + } + } + } + } + } + } + + private func generate(view: UIView, completion: @escaping (UIImage) -> Void) { + let size = CGSize(width: 360.0, height: 640.0) + UIGraphicsBeginImageContextWithOptions(size, false, 3.0) + view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in + if let cgImage = img?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }, opaque: true, scale: 1.0) + if let finalImage { + completion(finalImage) + } + } + + private func updateLayout(size: CGSize) { + self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate) + self.wallpaperBackgroundNode.frame = CGRect(origin: .zero, size: size) + self.darkWallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate) + self.darkWallpaperBackgroundNode.frame = CGRect(origin: .zero, size: size) + } +} + +public final class DrawingMessageRenderer { + class ContainerNode: ASDisplayNode { + private let context: AccountContext + private let messages: [Message] + private let isNight: Bool + + private let messagesContainerNode: ASDisplayNode + private var avatarHeaderNode: ListViewItemHeaderNode? + private var messageNodes: [ListViewItemNode]? + + init(context: AccountContext, messages: [Message], isNight: Bool = false) { + self.context = context + self.messages = messages + self.isNight = isNight + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + super.init() + + self.addSubnode(self.messagesContainerNode) + } + + public func render(completion: @escaping (CGSize, UIImage?) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let defaultPresentationData = defaultPresentationData() + + var mockPresentationData = PresentationData( + strings: presentationData.strings, + theme: defaultPresentationTheme, + autoNightModeTriggered: false, + chatWallpaper: presentationData.chatWallpaper, + chatFontSize: defaultPresentationData.chatFontSize, + chatBubbleCorners: defaultPresentationData.chatBubbleCorners, + listsFontSize: defaultPresentationData.listsFontSize, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + nameSortOrder: presentationData.nameSortOrder, + reduceMotion: false, + largeEmoji: true + ) + + if self.isNight { + let darkTheme = defaultDarkColorPresentationTheme + mockPresentationData = mockPresentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkTheme.chat.defaultWallpaper) + } + + let layout = ContainerViewLayout(size: CGSize(width: 360.0, height: 640.0), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: .zero, safeInsets: .zero, additionalInsets: .zero, statusBarHeight: 0.0, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) + let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) + + Queue.mainQueue().after(0.05, { + self.generate(size: size) { image in + completion(size, image) + } + }) + } + + private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { + UIGraphicsBeginImageContextWithOptions(size, false, 3.0) + self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = img?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }, opaque: false, scale: 1.0) + if let finalImage { + completion(finalImage) + } + } + + private func updateMessagesLayout(layout: ContainerViewLayout, presentationData: PresentationData) -> CGSize { + let size = layout.size + + let theme = presentationData.theme.withUpdated(preview: true) + + let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) + + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] + + let inset: CGFloat = 16.0 + let leftInset: CGFloat = 37.0 + let containerWidth = layout.size.width - inset * 2.0 + let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + + var width: CGFloat = containerWidth + var height: CGFloat = size.height + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + } + self.messageNodes = messageNodes + } + + if let messageNodes = self.messageNodes { + var minX: CGFloat = .greatestFiniteMagnitude + var maxX: CGFloat = -.greatestFiniteMagnitude + var minY: CGFloat = .greatestFiniteMagnitude + var maxY: CGFloat = -.greatestFiniteMagnitude + for node in messageNodes { + if node.frame.minY < minY { + minY = node.frame.minY + } + if node.frame.maxY > maxY { + maxY = node.frame.maxY + } + if let areaNode = node.subnodes?.last { + if areaNode.frame.minX < minX { + minX = areaNode.frame.minX + } + if areaNode.frame.maxX > maxX { + maxX = areaNode.frame.maxX + } + } + } + width = abs(maxX - minX) + height = abs(maxY - minY) + } + + var bottomOffset: CGFloat = 0.0 + if let messageNodes = self.messageNodes { + for itemNode in messageNodes { + itemNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: itemNode.frame.size) + bottomOffset += itemNode.frame.maxY + itemNode.updateFrame(itemNode.frame, within: layout.size) + } + } + + let avatarHeaderNode: ListViewItemHeaderNode + if let currentAvatarHeaderNode = self.avatarHeaderNode { + avatarHeaderNode = currentAvatarHeaderNode + avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem) + } else { + avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true) + avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(avatarHeaderNode) + self.avatarHeaderNode = avatarHeaderNode + } + + avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) + avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) + + let containerSize = CGSize(width: width + leftInset + 6.0, height: height) + self.frame = CGRect(origin: CGPoint(), size: containerSize) + self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize) + + return containerSize + } + } + + private let context: AccountContext + private let messages: [Message] + + private let dayContainerNode: ContainerNode + private let nightContainerNode: ContainerNode + + public init(context: AccountContext, messages: [Message]) { + self.context = context + self.messages = messages + + self.dayContainerNode = ContainerNode(context: context, messages: messages) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) + } + + public func render(completion: @escaping (CGSize, UIImage?, UIImage?) -> Void) { + var finalSize: CGSize = .zero + var dayImage: UIImage? + var nightImage: UIImage? + + let completeIfReady = { + if let dayImage, let nightImage { + completion(finalSize, dayImage, nightImage) + } + } + self.dayContainerNode.render { size, image in + finalSize = size + dayImage = image + completeIfReady() + } + self.nightContainerNode.render { size, image in + finalSize = size + nightImage = image + completeIfReady() + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 3baa09442c0..de2553c9d9a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -6,6 +6,7 @@ import Vision import Photos import SwiftSignalKit import Display +import Postbox import TelegramCore import TelegramPresentationData import FastBlur @@ -88,6 +89,7 @@ public final class MediaEditor { case video(String, UIImage?, Bool, String?, PixelDimensions, Double) case asset(PHAsset) case draft(MediaEditorDraft) + case message(MessageId) var dimensions: PixelDimensions { switch self { @@ -97,6 +99,8 @@ public final class MediaEditor { return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) case let .draft(draft): return draft.dimensions + case .message: + return PixelDimensions(width: 1080, height: 1920) } } } @@ -195,6 +199,8 @@ public final class MediaEditor { private let additionalPlayerPromise = Promise(nil) private let audioPlayerPromise = Promise(nil) + private var wallpapers: ((day: UIImage, night: UIImage?))? + private struct PlaybackState: Equatable { let duration: Double let position: Double @@ -407,6 +413,7 @@ public final class MediaEditor { additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, + nightTheme: false, drawing: nil, entities: [], toolValues: [:], @@ -467,11 +474,11 @@ public final class MediaEditor { let context = self.context let clock = self.clock - let textureSource: Signal<(UIImage?, AVPlayer?, AVPlayer?, GradientColors), NoError> + let textureSource: Signal<(UIImage?, UIImage?, AVPlayer?, AVPlayer?, GradientColors), NoError> switch subject { case let .image(image, _): let colors = mediaEditorGetGradientColors(from: image) - textureSource = .single((image, nil, nil, colors)) + textureSource = .single((image, nil, nil, nil, colors)) case let .draft(draft): if draft.isVideo { textureSource = Signal { subscriber in @@ -489,7 +496,7 @@ public final class MediaEditor { if let gradientColors = draft.values.gradientColors { let colors = GradientColors(top: gradientColors.first!, bottom: gradientColors.last!) - subscriber.putNext((nil, player, nil, colors)) + subscriber.putNext((nil, nil, player, nil, colors)) subscriber.putCompletion() return EmptyDisposable @@ -499,7 +506,7 @@ public final class MediaEditor { imageGenerator.maximumSize = CGSize(width: 72, height: 128) imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) - subscriber.putNext((nil, player, nil, colors)) + subscriber.putNext((nil, nil, player, nil, colors)) subscriber.putCompletion() } return ActionDisposable { @@ -517,7 +524,7 @@ public final class MediaEditor { } else { colors = mediaEditorGetGradientColors(from: image) } - textureSource = .single((image, nil, nil, colors)) + textureSource = .single((image, nil, nil, nil, colors)) } case let .video(path, transitionImage, mirror, _, _, _): let _ = mirror @@ -546,7 +553,7 @@ public final class MediaEditor { if let transitionImage { let colors = mediaEditorGetGradientColors(from: transitionImage) //TODO pass mirror - subscriber.putNext((nil, player, nil, colors)) + subscriber.putNext((nil, nil, player, nil, colors)) subscriber.putCompletion() return EmptyDisposable @@ -557,7 +564,7 @@ public final class MediaEditor { imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) //TODO pass mirror - subscriber.putNext((nil, player, nil, colors)) + subscriber.putNext((nil, nil, player, nil, colors)) subscriber.putCompletion() } return ActionDisposable { @@ -589,9 +596,9 @@ public final class MediaEditor { let additionalPlayerItem = AVPlayerItem(asset: asset) let additionalPlayer = AVPlayer(playerItem: additionalPlayerItem) additionalPlayer.automaticallyWaitsToMinimizeStalling = false - subscriber.putNext((nil, player, additionalPlayer, colors)) + subscriber.putNext((nil, nil, player, additionalPlayer, colors)) #else - subscriber.putNext((nil, player, nil, colors)) + subscriber.putNext((nil, nil, player, nil, colors)) #endif subscriber.putCompletion() } @@ -618,7 +625,7 @@ public final class MediaEditor { } if !degraded { let colors = mediaEditorGetGradientColors(from: image) - subscriber.putNext((image, nil, nil, colors)) + subscriber.putNext((image, nil, nil, nil, colors)) subscriber.putCompletion() } } @@ -628,17 +635,28 @@ public final class MediaEditor { } } } + case let .message(messageId): + textureSource = getChatWallpaperImage(context: self.context, messageId: messageId) + |> map { _, image, nightImage in + return (image, nightImage, nil, nil, GradientColors(top: .black, bottom: .black)) + } } self.textureSourceDisposable = (textureSource |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in if let self { - let (image, player, additionalPlayer, colors) = sourceAndColors + let (image, nightImage, player, additionalPlayer, colors) = sourceAndColors self.renderer.onNextRender = { [weak self] in self?.onFirstDisplay() } let textureSource = UniversalTextureSource(renderTarget: renderTarget) + + if case .message = self.self.subject { + if let image { + self.wallpapers = (image, nightImage ?? image) + } + } self.player = player self.playerPromise.set(.single(player)) @@ -647,7 +665,11 @@ public final class MediaEditor { self.additionalPlayerPromise.set(.single(additionalPlayer)) if let image { - textureSource.setMainInput(.image(image)) + if self.values.nightTheme, let nightImage { + textureSource.setMainInput(.image(nightImage)) + } else { + textureSource.setMainInput(.image(image)) + } } if let player, let playerItem = player.currentItem { textureSource.setMainInput(.video(playerItem)) @@ -662,8 +684,8 @@ public final class MediaEditor { if player == nil { self.updateRenderChain() - let _ = image -// self.maybeGeneratePersonSegmentation(image) +// let _ = image + self.maybeGeneratePersonSegmentation(image) } if let _ = self.values.audioTrack { @@ -934,6 +956,28 @@ public final class MediaEditor { } } + public func setNightTheme(_ nightTheme: Bool) { + self.updateValues(mode: .skipRendering) { values in + return values.withUpdatedNightTheme(nightTheme) + } + + guard let (dayImage, nightImage) = self.wallpapers, let nightImage else { + return + } + + if let textureSource = self.renderer.textureSource as? UniversalTextureSource { + if nightTheme { + textureSource.setMainInput(.image(nightImage)) + } else { + textureSource.setMainInput(.image(dayImage)) + } + } + } + + public func toggleNightTheme() { + self.setNightTheme(!self.values.nightTheme) + } + public enum PlaybackAction { case play case pause diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 397c299d5a1..14718adb060 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -72,10 +72,19 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti content = .file(file) case let .image(image, _): content = .image(image) + case let .animatedImage(data, _): + let _ = data + return [] case let .video(file): content = .video(file) case .dualVideoReference: return [] + case .message: + if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { + return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)] + } else { + return [] + } } return [MediaEditorComposerStickerEntity(postbox: postbox, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace, tintColor: tintColor, isStatic: entity.isExplicitlyStatic)] } @@ -142,6 +151,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { case file(TelegramMediaFile) case video(TelegramMediaFile) case image(UIImage) + case animatedImage([UIImage], Double) var file: TelegramMediaFile? { if case let .file(file) = self { @@ -270,6 +280,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { case let .image(image): self.isAnimated = false self.imagePromise.set(.single(image)) + case let .animatedImage(images, duration): + self.isAnimated = true + let _ = images + let _ = duration case .video: self.isAnimated = true } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 5d39ae1961f..8fadb762122 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -73,6 +73,7 @@ public final class MediaEditorDraft: Codable, Equatable { case values case caption case privacy + case forwardInfo case timestamp case locationLatitude case locationLongitude @@ -87,11 +88,12 @@ public final class MediaEditorDraft: Codable, Equatable { public let values: MediaEditorValues public let caption: NSAttributedString public let privacy: MediaEditorResultPrivacy? + public let forwardInfo: StoryId? public let timestamp: Int32 public let location: CLLocationCoordinate2D? public let expiresOn: Int32? - public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) { + public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, forwardInfo: StoryId?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) { self.path = path self.isVideo = isVideo self.thumbnail = thumbnail @@ -100,6 +102,7 @@ public final class MediaEditorDraft: Codable, Equatable { self.values = values self.caption = caption self.privacy = privacy + self.forwardInfo = forwardInfo self.timestamp = timestamp self.location = location self.expiresOn = expiresOn @@ -135,6 +138,8 @@ public final class MediaEditorDraft: Codable, Equatable { self.privacy = nil } + self.forwardInfo = try container.decodeIfPresent(StoryId.self, forKey: .forwardInfo) + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) ?? 1688909663 if let latitude = try container.decodeIfPresent(Double.self, forKey: .locationLatitude), let longitude = try container.decodeIfPresent(Double.self, forKey: .locationLongitude) { @@ -172,6 +177,8 @@ public final class MediaEditorDraft: Codable, Equatable { } else { try container.encodeNil(forKey: .privacy) } + try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) + try container.encode(self.timestamp, forKey: .timestamp) if let location = self.location { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift index 33ef99a3909..0270ec0a7ea 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -2,6 +2,10 @@ import Foundation import UIKit import AVFoundation import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramUIPreferences +import TelegramPresentationData extension AVPlayer { func fadeVolume(from: Float, to: Float, duration: Float, completion: (() -> Void)? = nil) -> SwiftSignalKit.Timer? { @@ -129,3 +133,111 @@ func getTextureImage(device: MTLDevice, texture: MTLTexture, mirror: Bool = fals } return UIImage(cgImage: cgImage) } + +public func getChatWallpaperImage(context: AccountContext, messageId: EngineMessage.Id) -> Signal<(CGSize, UIImage?, UIImage?), NoError> { + let themeSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) + |> map { sharedData -> PresentationThemeSettings in + let themeSettings: PresentationThemeSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) { + themeSettings = current + } else { + themeSettings = PresentationThemeSettings.defaultSettings + } + return themeSettings + } + + let peerWallpaper = context.account.postbox.transaction { transaction -> TelegramWallpaper? in + return (transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData)?.wallpaper + } + + return combineLatest(themeSettings, peerWallpaper) + |> mapToSignal { themeSettings, peerWallpaper -> Signal<(TelegramWallpaper?, TelegramWallpaper?), NoError> in + var currentColors = themeSettings.themeSpecificAccentColors[themeSettings.theme.index] + if let colors = currentColors, colors.baseColor == .theme { + currentColors = nil + } + + let themeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: themeSettings.theme, accentColor: currentColors)] ?? themeSettings.themeSpecificChatWallpapers[themeSettings.theme.index]) + + let dayWallpaper: TelegramWallpaper + if let themeSpecificWallpaper = themeSpecificWallpaper { + dayWallpaper = themeSpecificWallpaper + } else { + let theme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, preview: true) ?? defaultPresentationTheme + dayWallpaper = theme.chat.defaultWallpaper + } + + var nightWallpaper: TelegramWallpaper? + + let automaticTheme = themeSettings.automaticThemeSwitchSetting.theme + let effectiveColors = themeSettings.themeSpecificAccentColors[automaticTheme.index] + let nightThemeSpecificWallpaper = (themeSettings.themeSpecificChatWallpapers[coloredThemeIndex(reference: automaticTheme, accentColor: effectiveColors)] ?? themeSettings.themeSpecificChatWallpapers[automaticTheme.index]) + + var preferredBaseTheme: TelegramBaseTheme? + if let baseTheme = themeSettings.themePreferredBaseTheme[automaticTheme.index], [.night, .tinted].contains(baseTheme) { + preferredBaseTheme = baseTheme + } else { + preferredBaseTheme = .night + } + + let darkTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: automaticTheme, baseTheme: preferredBaseTheme, accentColor: effectiveColors?.color, bubbleColors: effectiveColors?.customBubbleColors ?? [], wallpaper: effectiveColors?.wallpaper, baseColor: effectiveColors?.baseColor, serviceBackgroundColor: defaultServiceBackgroundColor) ?? defaultPresentationTheme + + if let nightThemeSpecificWallpaper = nightThemeSpecificWallpaper { + nightWallpaper = nightThemeSpecificWallpaper + } else { + switch dayWallpaper { + case .builtin, .color, .gradient: + nightWallpaper = darkTheme.chat.defaultWallpaper + case .file: + if dayWallpaper.isPattern { + nightWallpaper = darkTheme.chat.defaultWallpaper + } else { + nightWallpaper = nil + } + default: + nightWallpaper = nil + } + } + + if let peerWallpaper { + if case let .emoticon(emoticon) = peerWallpaper { + return context.engine.themes.getChatThemes(accountManager: context.sharedContext.accountManager) + |> map { themes -> (TelegramWallpaper?, TelegramWallpaper?) in + if let theme = themes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) { + if let dayMatch = theme.settings?.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day }) { + if let peerDayWallpaper = dayMatch.wallpaper { + var peerNightWallpaper: TelegramWallpaper? + if let nightMatch = theme.settings?.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted }) { + peerNightWallpaper = nightMatch.wallpaper + } + return (peerDayWallpaper, peerNightWallpaper) + } else { + return (dayWallpaper, nightWallpaper) + } + } else { + return (dayWallpaper, nightWallpaper) + } + } else { + return (dayWallpaper, nightWallpaper) + } + } + } else { + return .single((peerWallpaper, nil)) + } + } else { + return .single((dayWallpaper, nightWallpaper)) + } + } + |> mapToSignal { dayWallpaper, nightWallpaper -> Signal<(CGSize, UIImage?, UIImage?), NoError> in + return Signal { subscriber in + Queue.mainQueue().async { + let wallpaperRenderer = DrawingWallpaperRenderer(context: context, dayWallpaper: dayWallpaper, nightWallpaper: nightWallpaper) + wallpaperRenderer.render { size, image, darkImage, mediaRect in + subscriber.putNext((size, image, darkImage)) + subscriber.putCompletion() + } + } + return EmptyDisposable + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 6fa55dd0bbc..9d8676a5be2 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -315,6 +315,9 @@ public final class MediaEditorValues: Codable, Equatable { if lhs.audioTrackSamples != rhs.audioTrackSamples { return false } + if lhs.nightTheme != rhs.nightTheme { + return false + } for key in EditorToolKey.allCases { let lhsToolValue = lhs.toolValues[key] @@ -373,6 +376,7 @@ public final class MediaEditorValues: Codable, Equatable { case additionalVideoOffset case additionalVideoVolume + case nightTheme case drawing case entities case toolValues @@ -412,6 +416,7 @@ public final class MediaEditorValues: Codable, Equatable { public let additionalVideoOffset: Double? public let additionalVideoVolume: CGFloat? + public let nightTheme: Bool public let drawing: UIImage? public let entities: [CodableDrawingEntity] public let toolValues: [EditorToolKey: Any] @@ -452,6 +457,7 @@ public final class MediaEditorValues: Codable, Equatable { additionalVideoTrimRange: Range?, additionalVideoOffset: Double?, additionalVideoVolume: CGFloat?, + nightTheme: Bool, drawing: UIImage?, entities: [CodableDrawingEntity], toolValues: [EditorToolKey: Any], @@ -485,6 +491,7 @@ public final class MediaEditorValues: Codable, Equatable { self.additionalVideoTrimRange = additionalVideoTrimRange self.additionalVideoOffset = additionalVideoOffset self.additionalVideoVolume = additionalVideoVolume + self.nightTheme = nightTheme self.drawing = drawing self.entities = entities self.toolValues = toolValues @@ -534,6 +541,7 @@ public final class MediaEditorValues: Codable, Equatable { self.additionalVideoOffset = try container.decodeIfPresent(Double.self, forKey: .additionalVideoOffset) self.additionalVideoVolume = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoVolume) + self.nightTheme = try container.decodeIfPresent(Bool.self, forKey: .nightTheme) ?? false if let drawingData = try container.decodeIfPresent(Data.self, forKey: .drawing), let image = UIImage(data: drawingData) { self.drawing = image } else { @@ -595,6 +603,7 @@ public final class MediaEditorValues: Codable, Equatable { try container.encodeIfPresent(self.additionalVideoOffset, forKey: .additionalVideoOffset) try container.encodeIfPresent(self.additionalVideoVolume, forKey: .additionalVideoVolume) + try container.encode(self.nightTheme, forKey: .nightTheme) if let drawing = self.drawing, let pngDrawingData = drawing.pngData() { try container.encode(pngDrawingData, forKey: .drawing) } @@ -618,85 +627,93 @@ public final class MediaEditorValues: Codable, Equatable { } public func makeCopy() -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedVideoIsMirrored(_ videoIsMirrored: Bool) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedVideoVolume(_ videoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideo(path: String?, isDual: Bool, positionChanges: [VideoPositionChange]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: path, additionalVideoIsDual: isDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoTrimRange(_ additionalVideoTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoOffset(_ additionalVideoOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAdditionalVideoVolume(_ additionalVideoVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrack(_ audioTrack: MediaAudioTrack?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackTrimRange(_ audioTrackTrimRange: Range?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackOffset(_ audioTrackOffset: Double?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackVolume(_ audioTrackVolume: CGFloat?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } func withUpdatedAudioTrackSamples(_ audioTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues { - return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, qualityPreset: self.qualityPreset) + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, qualityPreset: self.qualityPreset) + } + + func withUpdatedNightTheme(_ nightTheme: Bool) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + } + + public func withUpdatedEntities(_ entities: [CodableDrawingEntity]) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } public var resultDimensions: PixelDimensions { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index 3c6dec501ef..ac834f1ed86 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -3,6 +3,7 @@ import UIKit import Display import CoreLocation import Photos +import Postbox import TelegramCore import AccountContext import MediaEditor @@ -20,22 +21,38 @@ extension MediaEditorScreen { let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - let caption = self.getCaption() + let filteredEntities = self.node.entitiesView.entities.filter { entity in + if entity is DrawingMediaEntity { + return false + } else if let entity = entity as? DrawingStickerEntity, case .message = entity.content { + return false + } + return true + } - if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false && caption.string.isEmpty { - return false + let values = mediaEditor.values + let filteredValues = values.withUpdatedEntities([]) + + let caption = self.getCaption() + if let subject = self.node.subject { + if case .asset = subject, !values.hasChanges && caption.string.isEmpty { + return false + } else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { + return false + } } return true } func saveDraft(id: Int64?) { - guard let subject = self.node.subject, let mediaEditor = self.node.mediaEditor else { + guard let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { return } try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) let values = mediaEditor.values let privacy = self.state.privacy + let forwardSource = self.forwardSource let caption = self.getCaption() let duration = mediaEditor.duration ?? 0.0 @@ -43,7 +60,7 @@ extension MediaEditorScreen { var timestamp: Int32 var location: CLLocationCoordinate2D? let expiresOn: Int32 - if case let .draft(draft, _) = subject { + if case let .draft(draft, _) = actualSubject { timestamp = draft.timestamp location = draft.location if let _ = id { @@ -69,29 +86,74 @@ extension MediaEditorScreen { guard let resultImage else { return } - let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) - - let context = self.context - let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in - if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = "\(Int64.random(in: .min ... .max)).jpg" - if let data = image.jpegData(compressionQuality: 0.87) { - let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) - try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) - if let id { - saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) - } else { - addStoryDraft(engine: context.engine, item: draft) - } + enum MediaInput { + case image(image: UIImage, dimensions: PixelDimensions) + case video(path: String, dimensions: PixelDimensions, duration: Double) + + var isVideo: Bool { + switch self { + case .video: + return true + case .image: + return false + } + } + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions): + return dimensions + case let .video(_, dimensions, _): + return dimensions + } + } + + var duration: Double? { + switch self { + case .image: + return nil + case let .video(_, _, duration): + return duration + } + } + + var fileExtension: String { + switch self { + case .image: + return "jpg" + case .video: + return "mp4" } } } - let saveVideoDraft: (String, PixelDimensions, Double) -> Void = { videoPath, dimensions, duration in + let context = self.context + func innerSaveDraft(media: MediaInput) { + let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = "\(Int64.random(in: .min ... .max)).mp4" - let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) - try? FileManager.default.copyItem(atPath: videoPath, toPath: draft.fullPath(engine: context.engine)) + let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)" + let draft = MediaEditorDraft( + path: path, + isVideo: media.isVideo, + thumbnail: thumbnailImage, + dimensions: media.dimensions, + duration: media.duration, + values: values, + caption: caption, + privacy: privacy, + forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) }, + timestamp: timestamp, + location: location, + expiresOn: expiresOn + ) + switch media { + case let .image(image, _): + if let data = image.jpegData(compressionQuality: 0.87) { + try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) + } + case let .video(path, _, _): + try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine)) + } if let id { saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) } else { @@ -102,14 +164,14 @@ extension MediaEditorScreen { switch subject { case let .image(image, dimensions, _, _): - saveImageDraft(image, dimensions) + innerSaveDraft(media: .image(image: image, dimensions: dimensions)) case let .video(path, _, _, _, _, dimensions, _, _, _): - saveVideoDraft(path, dimensions, duration) + innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) case let .asset(asset): if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let urlAsset = avAsset as? AVURLAsset { - saveVideoDraft(urlAsset.url.relativePath, PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration) + innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) } } } else { @@ -117,16 +179,23 @@ extension MediaEditorScreen { options.deliveryMode = .highQualityFormat PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in if let image { - saveImageDraft(image, PixelDimensions(image.size)) + innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) } } } case let .draft(draft, _): if draft.isVideo { - saveVideoDraft(draft.fullPath(engine: context.engine), draft.dimensions, draft.duration ?? 0.0) + innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { - saveImageDraft(image, draft.dimensions) + innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) } + case .message: + if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { + innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) + } + } + + if case let .draft(draft, _) = actualSubject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) } }) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 6d47bfe1f42..e9a09fad9f1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -45,6 +45,7 @@ private let muteButtonTag = GenericComponentViewTag() private let saveButtonTag = GenericComponentViewTag() private let switchCameraButtonTag = GenericComponentViewTag() private let stickerButtonTag = GenericComponentViewTag() +private let dayNightButtonTag = GenericComponentViewTag() final class MediaEditorScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -235,6 +236,7 @@ final class MediaEditorScreenComponent: Component { var muteDidChange = false var playbackDidChange = false + var dayNightDidChange = false } func makeState() -> State { @@ -263,6 +265,7 @@ final class MediaEditorScreenComponent: Component { private let playbackButton = ComponentView() private let muteButton = ComponentView() private let saveButton = ComponentView() + private let dayNightButton = ComponentView() private let switchCameraButton = ComponentView() @@ -665,7 +668,7 @@ final class MediaEditorScreenComponent: Component { if self.component == nil { if let initialCaption = controller.initialCaption { self.inputPanelExternalState.initialText = initialCaption - } else if case let .draft(draft, _) = controller.node.subject { + } else if case let .draft(draft, _) = controller.node.actualSubject { self.inputPanelExternalState.initialText = draft.caption } } @@ -1572,6 +1575,102 @@ final class MediaEditorScreenComponent: Component { } var topButtonOffsetX: CGFloat = 0.0 + + if let subject = controller.node.subject, case .message = subject { + let isNightTheme = mediaEditor?.values.nightTheme == true + + let dayNightContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + dayNightContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: isNightTheme ? "anim_sun" : "anim_sun_reverse", + mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(dayNightButtonTag) + ) + ) + } else { + dayNightContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let dayNightButtonSize = self.dayNightButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: dayNightContentComponent, + action: { [weak self, weak state, weak mediaEditor] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } + + if let mediaEditor { + state?.dayNightDidChange = true + + if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) { + controller.node.previewContainerView.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + + Queue.mainQueue().after(0.1) { + mediaEditor.toggleNightTheme() + controller.node.entitiesView.eachView { view in + if let stickerEntityView = view as? DrawingStickerEntityView { + stickerEntityView.isNightTheme = mediaEditor.values.nightTheme + } + } + } + } + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let dayNightButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: dayNightButtonSize + ) + if let dayNightButtonView = self.dayNightButton.view { + if dayNightButtonView.superview == nil { + setupButtonShadow(dayNightButtonView) + self.addSubview(dayNightButtonView) + + dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center) + transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size)) + transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil { + dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in + dayNightButtonView?.removeFromSuperview() + }) + dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + if let playerState = state.playerState, playerState.hasAudio { let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false @@ -1629,7 +1728,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 44.0, height: 44.0) ) let muteButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: muteButtonSize ) if let muteButtonView = self.muteButton.view { @@ -1722,6 +1821,8 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) } + + topButtonOffsetX += 50.0 } else { if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in @@ -1935,6 +2036,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private let initializationTimestamp = CACurrentMediaTime() var subject: MediaEditorScreen.Subject? + var actualSubject: MediaEditorScreen.Subject? + private var subjectDisposable: Disposable? private var appInForegroundDisposable: Disposable? @@ -1945,7 +2048,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let storyPreview: ComponentView fileprivate let toolValue: ComponentView - private let previewContainerView: UIView + fileprivate let previewContainerView: UIView private var transitionInView: UIImageView? private let gradientView: UIImageView @@ -1979,16 +2082,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissed = false private var isDismissBySwipeSuppressed = false - fileprivate var hasAnyChanges = false + private (set) var hasAnyChanges = false private var playbackPositionDisposable: Disposable? - var recording: MediaEditorScreen.Recording private var presentationData: PresentationData private var validLayout: ContainerViewLayout? + private let readyValue = Promise() + init(controller: MediaEditorScreen) { self.controller = controller self.context = controller.context @@ -2095,6 +2199,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate stickerItems ) |> map { emoji, stickers -> StickerPickerInputData in return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil) + } |> afterNext { [weak self] _ in + if let self { + self.controller?.checkPostingAvailability() + } } stickerPickerInputData.set(signal) @@ -2164,11 +2272,27 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setup(with subject: MediaEditorScreen.Subject) { - self.subject = subject + self.actualSubject = subject + + var effectiveSubject = subject + if case let .draft(draft, _ ) = subject { + for entity in draft.values.entities { + if case let .sticker(sticker) = entity, case let .message(ids, _, _) = sticker.content { + effectiveSubject = .message(ids) + break + } + } + } + self.subject = effectiveSubject + guard let controller = self.controller else { return } + Queue.mainQueue().justDispatch { + controller.setupAudioSessionIfNeeded() + } + if case let .draft(draft, _) = subject, let privacy = draft.privacy { controller.state.privacy = privacy } @@ -2181,16 +2305,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isFromCamera = true case .draft: isSavingAvailable = true + case .message: + isSavingAvailable = true default: isSavingAvailable = false } controller.isSavingAvailable = isSavingAvailable controller.requestLayout(transition: .immediate) - let mediaDimensions = subject.dimensions + let mediaDimensions = effectiveSubject.dimensions let maxSide: CGFloat = 1920.0 / UIScreen.main.scale let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) - let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) + let mediaEntity = DrawingMediaEntity(size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) if fittedSize.height > fittedSize.width { mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) @@ -2242,10 +2368,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(context: self.context, subject: subject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor(context: self.context, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } + if case .message = subject, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { + mediaEditor.setNightTheme(true) + } mediaEditor.attachPreviewView(self.previewView) mediaEditor.valuesUpdated = { [weak self] values in if let self, let controller = self.controller, values.gradientColors != nil, controller.previousSavedValues != values { @@ -2260,7 +2389,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - if case let .image(_, _, additionalImage, position) = subject, let additionalImage { + if case .message = effectiveSubject { + } else { + self.readyValue.set(.single(true)) + } + + if case let .image(_, _, additionalImage, position) = effectiveSubject, let additionalImage { let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) @@ -2276,7 +2410,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate imageEntity.scale = 1.625 imageEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(imageEntity, announce: false) - } else if case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position) = subject { + } else if case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position) = effectiveSubject { mediaEditor.setVideoIsMirrored(mirror) if let additionalVideoPath { let videoEntity = DrawingStickerEntity(content: .dualVideoReference(false)) @@ -2295,6 +2429,58 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } + } else if case let .message(messageIds) = effectiveSubject { + let isNightTheme = mediaEditor.values.nightTheme + let _ = ((self.context.engine.data.get( + EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + )) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + var messages: [Message] = [] + for id in messageIds { + if let maybeMessage = result[id], let message = maybeMessage { + messages.append(message._asMessage()) + } + } + + let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile + + let renderer = DrawingMessageRenderer(context: self.context, messages: messages) + renderer.render(completion: { size, dayImage, nightImage in + if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { + return true + } else { + return false + } + }) as? DrawingStickerEntityView { + existingEntityView.isNightTheme = isNightTheme + let messageEntity = existingEntityView.entity as! DrawingStickerEntity + messageEntity.renderImage = dayImage + messageEntity.secondaryRenderImage = nightImage + existingEntityView.update(animated: false) + } else { + let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size)) + messageEntity.renderImage = dayImage + messageEntity.secondaryRenderImage = nightImage + messageEntity.referenceDrawingSize = storyDimensions + messageEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + + let fraction = max(size.width, size.height) / 353.0 + messageEntity.scale = min(6.0, 3.3 * fraction) + + if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { + if isNightTheme { + entityView.isNightTheme = true + } + } + } + + self.readyValue.set(.single(true)) + }) + }) } self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in @@ -2327,7 +2513,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if controller.isEmbeddedEditor == true { mediaEditor.onFirstDisplay = { [weak self] in if let self { - if subject.isPhoto { + if effectiveSubject.isPhoto { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false @@ -2539,14 +2725,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return false } } + if self.stickerScreen != nil { + return false + } return true } else { return true } } - private var enhanceInitialTranslation: Float? + private var canEnhance: Bool { + if case .message = self.subject { + return false + } + return true + } + private var enhanceInitialTranslation: Float? @objc func handleDismissPan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let controller = self.controller, let layout = self.validLayout, (layout.inputHeight ?? 0.0).isZero else { return @@ -2571,7 +2766,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isDismissBySwipeSuppressed = controller.isEligibleForDraft() controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } - } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing { + } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing && self.canEnhance { self.isEnhancing = true controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } @@ -2617,7 +2812,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isDismissing = false controller.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } - } else { + } else if self.isEnhancing { self.isEnhancing = false Queue.mainQueue().after(0.5) { controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) @@ -2629,14 +2824,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { + return + } self.entitiesView.handlePan(gestureRecognizer) } @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { + return + } self.entitiesView.handlePinch(gestureRecognizer) } @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { + return + } self.entitiesView.handleRotate(gestureRecognizer) } @@ -2787,7 +2991,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } else { - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + if case .message = self.actualSubject, let layout = self.validLayout { + self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + completion() + } else if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .camera, completion: completion) } } @@ -2810,7 +3017,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.15) var isNew: Bool? = false - if let subject = self.subject { + if let subject = self.actualSubject { if saveDraft { isNew = true } @@ -3197,7 +3404,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } var location: CLLocationCoordinate2D? - if let subject = self.subject { + if let subject = self.actualSubject { if case let .asset(asset) = subject { location = asset.location?.coordinate } else if case let .draft(draft, _) = subject { @@ -3843,7 +4050,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) } - let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor) + let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor, hiddenTools: !self.canEnhance ? [.enhance] : []) controller.dismissed = { [weak self] in if let self { self.animateInFromTool() @@ -3952,7 +4159,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) if isFirstTime { - self.animateIn() + self.isHidden = true + let _ = (self.readyValue.get() + |> take(1)).start(next: { [weak self] _ in + if let self { + self.isHidden = false + self.animateIn() + } + }) } } } @@ -3988,6 +4202,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case video(String, UIImage?, Bool, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft, Int64?) + case message([MessageId]) var dimensions: PixelDimensions { switch self { @@ -3997,6 +4212,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) case let .draft(draft, _): return draft.dimensions + case .message: + return PixelDimensions(width: 1080, height: 1920) } } @@ -4010,19 +4227,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .asset(asset) case let .draft(draft, _): return .draft(draft) - } - } - - var mediaContent: DrawingMediaEntity.Content { - switch self { - case let .image(image, dimensions, _, _): - return .image(image, dimensions) - case let .video(videoPath, _, _, _, _, dimensions, _, _, _): - return .video(videoPath, dimensions) - case let .asset(asset): - return .asset(asset) - case let .draft(draft, _): - return .image(draft.thumbnail, draft.dimensions) + case let .message(messageIds): + return .message(messageIds.first!) } } @@ -4040,6 +4246,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return asset.mediaType == .video case let .draft(draft, _): return draft.isVideo + case .message: + return false } } } @@ -4091,7 +4299,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() private var postingAvailabilityDisposable: Disposable? - + public init( context: AccountContext, subject: Signal, @@ -4162,10 +4370,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate updateStorySources(engine: self.context.engine) updateStoryDrafts(engine: self.context.engine) - - if let _ = forwardSource { - self.postingAvailabilityPromise.set(self.context.engine.messages.checkStoriesUploadAvailability(target: .myStories)) - } } required public init(coder aDecoder: NSCoder) { @@ -4178,6 +4382,91 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.postingAvailabilityDisposable?.dispose() } + fileprivate func setupAudioSessionIfNeeded() { + guard let subject = self.node.subject else { + return + } + var needsAudioSession = false + var checkPostingAvailability = false + if self.forwardSource != nil { + needsAudioSession = true + checkPostingAvailability = true + } + if self.isEditingStory { + needsAudioSession = true + } + if case .message = subject { + needsAudioSession = true + checkPostingAvailability = true + } + if needsAudioSession { + self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in + if #available(iOS 13.0, *) { + try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) + } + }, deactivate: { _ in + return .single(Void()) + }) + } + if checkPostingAvailability { + self.postingAvailabilityPromise.set(self.context.engine.messages.checkStoriesUploadAvailability(target: .myStories)) + } + } + + fileprivate func checkPostingAvailability() { + guard self.postingAvailabilityDisposable == nil else { + return + } + self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] availability in + guard let self, availability != .available else { + return + } + + let subject: PremiumLimitSubject + switch availability { + case .expiringLimit: + subject = .expiringStories + case .weeklyLimit: + subject = .storiesWeekly + case .monthlyLimit: + subject = .storiesMonthly + default: + subject = .expiringStories + } + + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: subject, count: 10, forceDark: true, cancel: { [weak self] in + self?.requestDismiss(saveDraft: false, animated: true) + }, action: { [weak self] in + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: { [weak self] in + guard let self else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + let isPremium = peer?.isPremium ?? false + if !isPremium { + self.requestDismiss(saveDraft: false, animated: true) + } + }) + }) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(controller) + } + }) + } + override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -4189,65 +4478,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate Queue.mainQueue().after(0.4) { self.adminedChannels.set(.single([]) |> then(self.context.engine.peers.channelsForStories())) self.closeFriends.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.CloseFriends())) - - if self.forwardSource != nil || self.isEditingStory { - self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in - if #available(iOS 13.0, *) { - try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) - } - }, deactivate: { _ in - return .single(Void()) - }) - } - - self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] availability in - guard let self, availability != .available else { - return - } - - let subject: PremiumLimitSubject - switch availability { - case .expiringLimit: - subject = .expiringStories - case .weeklyLimit: - subject = .storiesWeekly - case .monthlyLimit: - subject = .storiesMonthly - default: - subject = .expiringStories - } - - let context = self.context - var replaceImpl: ((ViewController) -> Void)? - let controller = self.context.sharedContext.makePremiumLimitController(context: self.context, subject: subject, count: 10, forceDark: true, cancel: { [weak self] in - self?.requestDismiss(saveDraft: false, animated: true) - }, action: { [weak self] in - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: { [weak self] in - guard let self else { - return - } - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self else { - return - } - let isPremium = peer?.isPremium ?? false - if !isPremium { - self.requestDismiss(saveDraft: false, animated: true) - } - }) - }) - replaceImpl?(controller) - return true - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { - navigationController.pushViewController(controller) - } - }) } } @@ -4668,7 +4898,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let title: String let save: String - if case .draft = self.node.subject { + if case .draft = self.node.actualSubject { title = presentationData.strings.Story_Editor_DraftDiscardDraft save = presentationData.strings.Story_Editor_DraftKeepDraft } else { @@ -4704,13 +4934,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.dismissAllTooltips() var showDraftTooltip = saveDraft - if let subject = self.node.subject, case .draft = subject { + if let subject = self.node.actualSubject, case .draft = subject { showDraftTooltip = false } if saveDraft { self.saveDraft(id: nil) } else { - if case let .draft(draft, id) = self.node.subject, id == nil { + if case let .draft(draft, id) = self.node.actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } @@ -4763,7 +4993,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var didComplete = false func requestCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, !self.didComplete else { + guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { return } @@ -4789,14 +5019,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var hasEntityChanges = false let randomId: Int64 - if case let .draft(_, id) = subject, let id { + if case let .draft(_, id) = actualSubject, let id { randomId = id } else { randomId = Int64.random(in: .min ... .max) } var mediaAreas: [MediaArea] = [] - if case let .draft(draft, _) = subject { + if case let .draft(draft, _) = actualSubject { if draft.values.entities != codableEntities { hasEntityChanges = true } @@ -4857,7 +5087,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var firstFrame: Signal<(UIImage?, UIImage?), NoError> let firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) - let videoResult: MediaResult.VideoResult + let videoResult: Signal var videoIsMirrored = false let duration: Double switch subject { @@ -4866,13 +5096,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let data = image.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: tempImagePath)) } - videoResult = .imageFile(path: tempImagePath) + videoResult = .single(.imageFile(path: tempImagePath)) duration = 5.0 firstFrame = .single((image, nil)) case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _): videoIsMirrored = mirror - videoResult = .videoFile(path: path) + videoResult = .single(.videoFile(path: path)) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { @@ -4914,7 +5144,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } case let .asset(asset): - videoResult = .asset(localIdentifier: asset.localIdentifier) + videoResult = .single(.asset(localIdentifier: asset.localIdentifier)) if asset.mediaType == .video { if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -4989,7 +5219,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case let .draft(draft, _): let draftPath = draft.fullPath(engine: context.engine) if draft.isVideo { - videoResult = .videoFile(path: draftPath) + videoResult = .single(.videoFile(path: draftPath)) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound } else { @@ -5010,7 +5240,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } else { - videoResult = .imageFile(path: draftPath) + videoResult = .single(.imageFile(path: draftPath)) duration = 5.0 if let image = UIImage(contentsOfFile: draftPath) { @@ -5019,11 +5249,41 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate firstFrame = .single((UIImage(), nil)) } } + case let .message(messages): + let isNightTheme = mediaEditor.values.nightTheme + let wallpaper = getChatWallpaperImage(context: self.context, messageId: messages.first!) + |> map { _, image, nightImage -> UIImage? in + if isNightTheme { + return nightImage ?? image + } else { + return image + } + } + + videoResult = wallpaper + |> mapToSignal { image in + if let image { + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" + if let data = image.jpegData(compressionQuality: 0.85) { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + return .single(.imageFile(path: tempImagePath)) + } else { + return .complete() + } + } + + firstFrame = wallpaper + |> map { image in + return (image, nil) + } + duration = 5.0 } - let _ = (firstFrame - |> deliverOnMainQueue).start(next: { [weak self] image, additionalImage in + let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) + .start(next: { [weak self] images, videoResult in if let self { + let (image, additionalImage) = images var currentImage = mediaEditor.resultImage if let image { mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true) @@ -5057,7 +5317,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) - if case let .draft(draft, id) = subject, id == nil { + if case let .draft(draft, id) = actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) } } else { @@ -5075,7 +5335,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) }) - if case let .draft(draft, id) = subject, id == nil { + if case let .draft(draft, id) = actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } @@ -5182,6 +5442,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fatalError() } } + case let .message(messages): + let isNightTheme = mediaEditor.values.nightTheme + exportSubject = getChatWallpaperImage(context: self.context, messageId: messages.first!) + |> mapToSignal { _, image, nightImage -> Signal in + if isNightTheme { + let effectiveImage = nightImage ?? image + return effectiveImage.flatMap({ .single(.image(image: $0)) }) ?? .complete() + } else { + return image.flatMap({ .single(.image(image: $0)) }) ?? .complete() + } + } } let _ = exportSubject.start(next: { [weak self] exportSubject in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index e5013f5429e..250f26efe5c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -127,17 +127,20 @@ private final class MediaToolsScreenComponent: Component { let context: AccountContext let mediaEditor: MediaEditor let section: MediaToolsSection + let hiddenTools: [EditorToolKey] let sectionUpdated: (MediaToolsSection) -> Void init( context: AccountContext, mediaEditor: MediaEditor, section: MediaToolsSection, + hiddenTools: [EditorToolKey], sectionUpdated: @escaping (MediaToolsSection) -> Void ) { self.context = context self.mediaEditor = mediaEditor self.section = section + self.hiddenTools = hiddenTools self.sectionUpdated = sectionUpdated } @@ -148,6 +151,9 @@ private final class MediaToolsScreenComponent: Component { if lhs.section != rhs.section { return false } + if lhs.hiddenTools != rhs.hiddenTools { + return false + } return true } @@ -362,7 +368,6 @@ private final class MediaToolsScreenComponent: Component { if availableSize.height < previewSize.height + 30.0 { topInset = 0.0 controlsBottomInset = -75.0 -// self.buttonsBackgroundView.backgroundColor = .black } else { self.buttonsBackgroundView.backgroundColor = .clear } @@ -658,6 +663,8 @@ private final class MediaToolsScreenComponent: Component { // ) ] + tools = tools.filter { !component.hiddenTools.contains($0.key) } + if !component.mediaEditor.sourceIsVideo { tools.insert(AdjustmentTool( key: .grain, @@ -1044,6 +1051,7 @@ public final class MediaToolsScreen: ViewController { context: self.context, mediaEditor: controller.mediaEditor, section: self.currentSection, + hiddenTools: controller.hiddenTools, sectionUpdated: { [weak self] section in if let self { self.currentSection = section @@ -1087,15 +1095,17 @@ public final class MediaToolsScreen: ViewController { } fileprivate let context: AccountContext + fileprivate let hiddenTools: [EditorToolKey] fileprivate let mediaEditor: MediaEditor public var dismissed: () -> Void = {} private var initialValues: MediaEditorValues - public init(context: AccountContext, mediaEditor: MediaEditor) { + public init(context: AccountContext, mediaEditor: MediaEditor, hiddenTools: [EditorToolKey]) { self.context = context self.mediaEditor = mediaEditor + self.hiddenTools = hiddenTools self.initialValues = mediaEditor.values.makeCopy() super.init(navigationBarPresentationData: nil) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 2d96d9672cc..6d096bf69ee 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -230,7 +230,7 @@ final class ContextResultPanelComponent: Component { presence: nil, selectionState: .none, hasNext: index != peers.count - 1, - action: { [weak self] peer in + action: { [weak self] peer, _, _ in guard let self, let component = self.component else { return } @@ -299,7 +299,7 @@ final class ContextResultPanelComponent: Component { presence: nil, selectionState: .none, hasNext: true, - action: { _ in + action: { _, _, _ in } )), environment: {}, diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 3f7c37cfd70..472def405ae 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -332,7 +332,7 @@ private func notificationsPeerCategoryEntries(peerId: EnginePeer.Id, notificatio } } existingThreadIds.insert(value.threadId) - entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) + entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) index += 1 } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD index 631aee7b7de..da94a4fdb11 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/Markdown", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/PagerComponent", "//submodules/PremiumUI", diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index a3c415cb829..031539d7337 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -20,82 +20,7 @@ import BundleIconComponent import AnimatedTextComponent import TextFormat import AudioToolbox - -private final class ButtonSubtitleComponent: CombinedComponent { - let count: Int - let theme: PresentationTheme - let strings: PresentationStrings - - init(count: Int, theme: PresentationTheme, strings: PresentationStrings) { - self.count = count - self.theme = theme - self.strings = strings - } - - static func ==(lhs: ButtonSubtitleComponent, rhs: ButtonSubtitleComponent) -> Bool { - if lhs.count != rhs.count { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - return true - } - - static var body: Body { - let icon = Child(BundleIconComponent.self) - let text = Child(AnimatedTextComponent.self) - - return { context in - let icon = icon.update( - component: BundleIconComponent( - name: "Chat/Input/Accessory Panels/TextLockIcon", - tintColor: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), - maxSize: CGSize(width: 10.0, height: 10.0) - ), - availableSize: CGSize(width: 100.0, height: 100.0), - transition: context.transition - ) - var textItems: [AnimatedTextComponent.Item] = [] - - let levelString = context.component.strings.ChannelReactions_LevelRequiredLabel("") - var previousIndex = 0 - let nsLevelString = levelString.string as NSString - for range in levelString.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if range.index == 0 { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .number(context.component.count, minDigits: 1))) - } - previousIndex = range.range.upperBound - } - if nsLevelString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable(100), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: nsLevelString.length - previousIndex))))) - } - - let text = text.update( - component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), items: textItems), - availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0), - transition: context.transition - ) - - let spacing: CGFloat = 3.0 - let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height) - context.add(icon - .position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center) - ) - context.add(text - .position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center) - ) - - return size - } - } -} +import PremiumLockButtonSubtitleComponent final class PeerAllowedReactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -904,7 +829,7 @@ final class PeerAllowedReactionsScreenComponent: Component { }).count : 0 if let boostStatus = self.boostStatus, customReactionCount > boostStatus.level { - buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(ButtonSubtitleComponent( + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(PremiumLockButtonSubtitleComponent( count: customReactionCount, theme: environment.theme, strings: environment.strings diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD new file mode 100644 index 00000000000..cfd1866200a --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerInfoChatListPaneNode", + module_name = "PeerInfoChatListPaneNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/TelegramStringFormatting", + "//submodules/ShimmerEffect", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/ChatListUI", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift new file mode 100644 index 00000000000..7fac23a272e --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -0,0 +1,206 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import TelegramStringFormatting +import ShimmerEffect +import ComponentFlow +import TelegramNotices +import TelegramUIPreferences +import AppBundle +import PeerInfoPaneNode +import ChatListUI + +public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private let context: AccountContext + + private let navigationController: () -> NavigationController? + + public weak var parentController: ViewController? + + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + public var isReady: Signal { + return self.ready.get() + } + + private let statusPromise = Promise(nil) + public var status: Signal { + self.statusPromise.get() + } + + public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var tabBarOffset: CGFloat { + return 0.0 + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let chatListNode: ChatListNode + + public init(context: AccountContext, navigationController: @escaping () -> NavigationController?) { + self.context = context + self.navigationController = navigationController + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.chatListNode = ChatListNode( + context: self.context, + location: .savedMessagesChats, + chatListFilter: nil, + previewing: false, + fillPreloadItems: false, + mode: .chatList(appendContacts: false), + isPeerEnabled: nil, + theme: self.presentationData.theme, + fontSize: self.presentationData.listsFontSize, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameSortOrder: self.presentationData.nameSortOrder, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + disableAnimations: false, + isInlineMode: false, + autoSetReady: false, + isMainTab: nil + ) + + super.init() + + self.addSubnode(self.chatListNode) + + self.presentationDataDisposable = (self.context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let self else { + return + } + self.presentationData = presentationData + }) + + self.ready.set(self.chatListNode.ready) + + self.chatListNode.peerSelected = { [weak self] peer, _, _, _, _ in + guard let self, let navigationController = self.navigationController() else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: self.context, + chatLocation: .replyThread(ChatReplyThreadMessage( + messageId: makeThreadIdMessageId( + peerId: self.context.account.peerId, + threadId: peer.id.toInt64() + ), + threadId: peer.id.toInt64(), + channelMessageId: nil, + isChannelPost: false, + isForumPost: false, + maxMessage: nil, + maxReadIncomingMessageId: nil, + maxReadOutgoingMessageId: nil, + unreadCount: 0, + initialFilledHoles: IndexSet(), + initialAnchor: .automatic, + isNotAvailable: false + )), + subject: nil, + keepStack: .always + )) + self.chatListNode.clearHighlightAnimated(true) + } + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + public func ensureMessageIsVisible(id: MessageId) { + } + + public func scrollToTop() -> Bool { + return false + } + + public func hitTestResultForScrolling() -> UIView? { + return nil + } + + public func brieflyDisableTouchActions() { + } + + public func findLoadedMessage(id: MessageId) -> Message? { + return nil + } + + public func updateHiddenMedia() { + } + + public func transferVelocity(_ velocity: CGFloat) { + } + + public func cancelPreviewGestures() { + } + + public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + public func addToTransitionSurface(view: UIView) { + } + + override public func didLoad() { + super.didLoad() + } + + + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { + let _ = otherGestureRecognizer + return true + } else { + return false + } + } + + public func updateSelectedMessages(animated: Bool) { + } + + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) + + transition.updateFrame(node: self.chatListNode, frame: CGRect(origin: CGPoint(), size: size)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.chatListNode.updateLayout( + transition: transition, + updateSizeAndInsets: ListViewUpdateSizeAndInsets( + size: size, + insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), + duration: duration, + curve: curve + ), + visibleTopInset: topInset, + originalTopInset: topInset, + storiesInset: 0.0, + inlineNavigationLocation: nil, + inlineNavigationTransitionFraction: 0.0 + ) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/BUILD new file mode 100644 index 00000000000..1e8570712a3 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerInfoPaneNode", + module_name = "PeerInfoPaneNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift new file mode 100644 index 00000000000..406286f904a --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -0,0 +1,57 @@ +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import TelegramPresentationData + +public enum PeerInfoPaneKey: Int32 { + case members + case stories + case media + case files + case music + case voice + case links + case gifs + case groupsInCommon + case recommended + case savedMessagesChats +} + +public struct PeerInfoStatusData: Equatable { + public var text: String + public var isActivity: Bool + public var key: PeerInfoPaneKey? + + public init( + text: String, + isActivity: Bool, + key: PeerInfoPaneKey? + ) { + self.text = text + self.isActivity = isActivity + self.key = key + } +} + +public protocol PeerInfoPaneNode: ASDisplayNode { + var isReady: Signal { get } + + var parentController: ViewController? { get set } + + var status: Signal { get } + var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } + var tabBarOffset: CGFloat { get } + + func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func scrollToTop() -> Bool + func transferVelocity(_ velocity: CGFloat) + func cancelPreviewGestures() + func findLoadedMessage(id: MessageId) -> Message? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + func addToTransitionSurface(view: UIView) + func updateHiddenMedia() + func updateSelectedMessages(animated: Bool) + func ensureMessageIsVisible(id: MessageId) +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index ab5e6546408..5a22b6a0917 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -142,6 +142,11 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/SolidRoundedButtonNode", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode", + "//submodules/MediaPickerUI", + "//submodules/AttachmentUI", + "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index dcf1946b95c..369f19bb3e0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -34,15 +34,17 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { let id: AnyHashable let label: Label let additionalBadgeLabel: String? + let additionalBadgeIcon: UIImage? let text: String let icon: UIImage? let iconSignal: Signal? let action: (() -> Void)? - init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal? = nil, action: (() -> Void)?) { + init(id: AnyHashable, label: Label = .none, additionalBadgeLabel: String? = nil, additionalBadgeIcon: UIImage? = nil, text: String, icon: UIImage? = nil, iconSignal: Signal? = nil, action: (() -> Void)?) { self.id = id self.label = label self.additionalBadgeLabel = additionalBadgeLabel + self.additionalBadgeIcon = additionalBadgeIcon self.text = text self.icon = icon self.iconSignal = iconSignal @@ -61,6 +63,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let labelBadgeNode: ASImageNode private let labelNode: ImmediateTextNode private var additionalLabelNode: ImmediateTextNode? + private var additionalLabelBadgeNode: ASImageNode? private let textNode: ImmediateTextNode private let arrowNode: ASImageNode private let bottomSeparatorNode: ASDisplayNode @@ -249,6 +252,24 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.removeFromSupernode() } + if let additionalBadgeIcon = item.additionalBadgeIcon { + let additionalLabelBadgeNode: ASImageNode + if let current = self.additionalLabelBadgeNode { + additionalLabelBadgeNode = current + } else { + additionalLabelBadgeNode = ASImageNode() + additionalLabelBadgeNode.isUserInteractionEnabled = false + self.additionalLabelBadgeNode = additionalLabelBadgeNode + self.insertSubnode(additionalLabelBadgeNode, belowSubnode: self.labelNode) + } + additionalLabelBadgeNode.image = additionalBadgeIcon + } else { + if let additionalLabelBadgeNode = self.additionalLabelBadgeNode { + self.additionalLabelBadgeNode = nil + additionalLabelBadgeNode.removeFromSupernode() + } + } + var badgeWidth = max(badgeDiameter, labelSize.width + 10.0) if case .semitransparentBadge = item.label { badgeWidth += 2.0 @@ -283,6 +304,11 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { additionalLabelNode.removeFromSupernode() } + if let additionalLabelBadgeNode = self.additionalLabelBadgeNode, let image = additionalLabelBadgeNode.image { + let additionalLabelSize = image.size + additionalLabelBadgeNode.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floor((height - additionalLabelSize.height) / 2.0) + 1.0), size: additionalLabelSize) + } + let labelBadgeNodeFrame: CGRect if case let .image(_, imageSize) = item.label { labelBadgeNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - imageSize.width, y: floorToScreenPixels(textFrame.midY - imageSize.height / 2.0)), size:imageSize) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift index 51aa52d85db..9788c296063 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift @@ -16,6 +16,7 @@ import ChatMessageInteractiveMediaBadge import SoftwareVideo import ChatControllerInteraction import PeerInfoVisualMediaPaneNode +import PeerInfoPaneNode private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift index 01c912103e5..7ce1f983973 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -13,6 +13,7 @@ import MergeLists import ItemListUI import ChatControllerInteraction import PeerInfoVisualMediaPaneNode +import PeerInfoPaneNode private struct GroupsInCommonListTransaction { let deletions: [ListViewDeleteItem] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift index 49574358fa9..7aa0d6f3e6f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoListPaneNode.swift @@ -18,6 +18,7 @@ import ChatPresentationInterfaceState import ChatControllerInteraction import PeerInfoVisualMediaPaneNode import ChatMessageItemView +import PeerInfoPaneNode final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index 3583fa23312..28750cf74c8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -13,6 +13,7 @@ import ItemListPeerActionItem import MergeLists import ItemListUI import PeerInfoVisualMediaPaneNode +import PeerInfoPaneNode private struct PeerMembersListTransaction { let deletions: [ListViewDeleteItem] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift index 031406638f9..3720a1935c3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedChannelsPane.swift @@ -18,6 +18,7 @@ import ChatControllerInteraction import MultilineTextComponent import Markdown import SolidRoundedButtonNode +import PeerInfoPaneNode private struct RecommendedChannelsListTransaction { let deletions: [ListViewDeleteItem] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index d9b5551ad86..658bf70aca8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -17,6 +17,7 @@ import AccountUtils import DeviceAccess import PeerInfoVisualMediaPaneNode import PhotoResources +import PeerInfoPaneNode enum PeerInfoUpdatingAvatar { case none @@ -189,6 +190,7 @@ final class TelegramGlobalSettings { final class PeerInfoScreenData { let peer: Peer? let chatPeer: Peer? + let savedMessagesPeer: Peer? let cachedData: CachedPeerData? let status: PeerInfoStatusData? let peerNotificationSettings: TelegramPeerNotificationSettings? @@ -223,6 +225,7 @@ final class PeerInfoScreenData { init( peer: Peer?, chatPeer: Peer?, + savedMessagesPeer: Peer?, cachedData: CachedPeerData?, status: PeerInfoStatusData?, peerNotificationSettings: TelegramPeerNotificationSettings?, @@ -246,6 +249,7 @@ final class PeerInfoScreenData { ) { self.peer = peer self.chatPeer = chatPeer + self.savedMessagesPeer = savedMessagesPeer self.cachedData = cachedData self.status = status self.peerNotificationSettings = peerNotificationSettings @@ -628,6 +632,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, return PeerInfoScreenData( peer: peer, chatPeer: peer, + savedMessagesPeer: nil, cachedData: peerView.cachedData, status: nil, peerNotificationSettings: nil, @@ -662,6 +667,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return .single(PeerInfoScreenData( peer: nil, chatPeer: nil, + savedMessagesPeer: nil, cachedData: nil, status: nil, peerNotificationSettings: nil, @@ -796,6 +802,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let savedMessagesPeer: Signal + if peerId == context.account.peerId, case let .replyThread(replyThreadMessage) = chatLocation { + savedMessagesPeer = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(replyThreadMessage.threadId))) + } else { + savedMessagesPeer = .single(nil) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder), @@ -803,9 +816,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen secretChatKeyFingerprint, status, hasStories, - accountIsPremium + accountIsPremium, + savedMessagesPeer ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, accountIsPremium, savedMessagesPeer -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { @@ -818,20 +832,20 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.append(.groupsInCommon) } } + + /*if peerId == context.account.peerId, case .peer = chatLocation { + availablePanes?.insert(.savedMessagesChats, at: 0) + }*/ } else { availablePanes = nil } - var peer: Peer? - peer = peerView.peers[userPeerId] - - /*if let user = peer as? TelegramUser, let profileColor = user.nameColor { - peer = user.withUpdatedProfileColor(PeerNameColor(rawValue: profileColor.rawValue)).withUpdatedProfileBackgroundEmojiId(user.backgroundEmojiId) - }*/ + let peer = peerView.peers[userPeerId] return PeerInfoScreenData( peer: peer, chatPeer: peerView.peers[peerId], + savedMessagesPeer: savedMessagesPeer?._asPeer(), cachedData: peerView.cachedData, status: status, peerNotificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, @@ -944,6 +958,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return PeerInfoScreenData( peer: peerView.peers[peerId], chatPeer: peerView.peers[peerId], + savedMessagesPeer: nil, cachedData: peerView.cachedData, status: status, peerNotificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, @@ -1069,7 +1084,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let threadData: Signal if case let .replyThread(message) = chatLocation { - let threadId = Int64(message.messageId.id) + let threadId = message.threadId let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId) threadData = context.account.postbox.combinedView(keys: [viewKey]) |> map { views -> MessageHistoryThreadData? in @@ -1156,6 +1171,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return .single(PeerInfoScreenData( peer: peerView.peers[groupId], chatPeer: peerView.peers[groupId], + savedMessagesPeer: nil, cachedData: peerView.cachedData, status: status, peerNotificationSettings: peerNotificationSettings, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift index a3a0913996d..566be835237 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -97,12 +97,14 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { let contextSourceNode: ContextReferenceContentNode private let textNode: ImmediateTextNode private let iconNode: ASImageNode + private let backIconLayer: SimpleShapeLayer private var animationNode: MoreIconNode? private let backgroundNode: NavigationBackgroundNode private var key: PeerInfoHeaderNavigationButtonKey? private var contentsColor: UIColor = .white + private var canBeExpanded: Bool = false var action: ((ASDisplayNode, ContextGesture?) -> Void)? @@ -117,6 +119,15 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true + self.backIconLayer = SimpleShapeLayer() + self.backIconLayer.lineWidth = 3.0 + self.backIconLayer.lineCap = .round + self.backIconLayer.lineJoin = .round + self.backIconLayer.strokeColor = UIColor.white.cgColor + self.backIconLayer.fillColor = nil + self.backIconLayer.isHidden = true + self.backIconLayer.path = try? convertSvgPath("M10.5,2 L1.5,11 L10.5,20 ") + self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true) super.init(pointerStyle: .insetRectangle(-8.0, 2.0)) @@ -128,6 +139,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.contextSourceNode.addSubnode(self.backgroundNode) self.contextSourceNode.addSubnode(self.textNode) self.contextSourceNode.addSubnode(self.iconNode) + self.contextSourceNode.layer.addSublayer(self.backIconLayer) self.addSubnode(self.containerNode) @@ -146,13 +158,44 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.action?(self.contextSourceNode, nil) } - func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var boundingRect = self.bounds + if self.textNode.alpha != 0.0 { + boundingRect = boundingRect.union(self.textNode.frame) + } + boundingRect = boundingRect.insetBy(dx: -8.0, dy: -4.0) + if boundingRect.contains(point) { + return super.hitTest(self.bounds.center, with: event) + } else { + return nil + } + } + + func updateContentsColor(backgroundColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) { self.contentsColor = contentsColor + self.canBeExpanded = canBeExpanded self.backgroundNode.updateColor(color: backgroundColor, transition: transition) transition.updateTintColor(layer: self.textNode.layer, color: self.contentsColor) transition.updateTintColor(layer: self.iconNode.layer, color: self.contentsColor) + transition.updateStrokeColor(layer: self.backIconLayer, strokeColor: self.contentsColor) + + switch self.key { + case .back: + transition.updateAlpha(layer: self.textNode.layer, alpha: canBeExpanded ? 1.0 : 0.0) + transition.updateTransformScale(node: self.textNode, scale: canBeExpanded ? 1.0 : 0.001) + + var iconTransform = CATransform3DIdentity + iconTransform = CATransform3DScale(iconTransform, canBeExpanded ? 1.0 : 0.8, canBeExpanded ? 1.0 : 0.8, 1.0) + iconTransform = CATransform3DTranslate(iconTransform, canBeExpanded ? -7.0 : 0.0, 0.0, 0.0) + transition.updateTransform(node: self.iconNode, transform: CATransform3DGetAffineTransform(iconTransform)) + + transition.updateTransform(layer: self.backIconLayer, transform: CATransform3DGetAffineTransform(iconTransform)) + transition.updateLineWidth(layer: self.backIconLayer, lineWidth: canBeExpanded ? 3.0 : 2.075) + default: + break + } if let animationNode = self.animationNode { transition.updateTintColor(layer: animationNode.imageNode.layer, color: self.contentsColor) @@ -184,9 +227,9 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { var animationState: MoreIconNodeState = .more switch key { case .back: - text = "" + text = presentationData.strings.Common_Back accessibilityText = presentationData.strings.Common_Back - icon = NavigationBar.thinBackArrowImage + icon = NavigationBar.backArrowImage(color: .white) case .edit: text = presentationData.strings.Common_Edit accessibilityText = text @@ -270,11 +313,19 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { } let inset: CGFloat = 0.0 + var textInset: CGFloat = 0.0 + switch key { + case .back: + textInset += 11.0 + default: + break + } let resultSize: CGSize - let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) - self.textNode.frame = textFrame + let textFrame = CGRect(origin: CGPoint(x: inset + textInset, y: floor((height - textSize.height) / 2.0)), size: textSize) + self.textNode.position = textFrame.center + self.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size) if let animationNode = self.animationNode { let animationSize = CGSize(width: 30.0, height: 30.0) @@ -286,7 +337,20 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) resultSize = size } else if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y) + let iconFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size).offsetBy(dx: iconOffset.x, dy: iconOffset.y) + self.iconNode.position = iconFrame.center + self.iconNode.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + if case .back = key { + self.backIconLayer.position = iconFrame.center + self.backIconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconNode.isHidden = true + self.backIconLayer.isHidden = false + } else { + self.iconNode.isHidden = false + self.backIconLayer.isHidden = true + } let size = CGSize(width: image.size.width + inset * 2.0, height: height) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index c12dd1e3601..7d6e435dfaa 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -36,18 +36,22 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { private var backgroundContentColor: UIColor = .clear private var contentsColor: UIColor = .white + private var canBeExpanded: Bool = false var performAction: ((PeerInfoHeaderNavigationButtonKey, ContextReferenceContentNode?, ContextGesture?) -> Void)? - func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, transition: ContainedViewLayoutTransition) { + func updateContentsColor(backgroundContentColor: UIColor, contentsColor: UIColor, canBeExpanded: Bool, transition: ContainedViewLayoutTransition) { self.backgroundContentColor = backgroundContentColor self.contentsColor = contentsColor + self.canBeExpanded = canBeExpanded for (_, button) in self.leftButtonNodes { - button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition) + button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition) + transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0)) } for (_, button) in self.rightButtonNodes { - button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: transition) + button.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: canBeExpanded, transition: transition) + transition.updateSublayerTransformOffset(layer: button.layer, offset: CGPoint(x: canBeExpanded ? 8.0 : 0.0, y: 0.0)) } } @@ -106,7 +110,9 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { buttonNode.frame = buttonFrame buttonNode.alpha = 0.0 transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate) + buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate) + + transition.updateSublayerTransformOffset(layer: buttonNode.layer, offset: CGPoint(x: canBeExpanded ? -8.0 : 0.0, y: 0.0)) } else { transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) @@ -202,7 +208,7 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { } let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) if wasAdded { - buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, transition: .immediate) + buttonNode.updateContentsColor(backgroundColor: self.backgroundContentColor, contentsColor: self.contentsColor, canBeExpanded: self.canBeExpanded, transition: .immediate) if key == .moreToSearch { buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) @@ -211,6 +217,8 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { buttonNode.frame = buttonFrame buttonNode.alpha = 0.0 transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + + transition.updateSublayerTransformOffset(layer: buttonNode.layer, offset: CGPoint(x: canBeExpanded ? 8.0 : 0.0, y: 0.0)) } else { transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 2a9dcd5fe19..7a04230755a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -39,6 +39,7 @@ import ComponentDisplayAdapters import ChatAvatarNavigationNode import MultiScaleTextNode import PeerInfoCoverComponent +import PeerInfoPaneNode final class PeerInfoHeaderNavigationTransition { let sourceNavigationBar: NavigationBar @@ -110,10 +111,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { let titleNodeContainer: ASDisplayNode let titleNodeRawContainer: ASDisplayNode let titleNode: MultiScaleTextNode + let titleCredibilityIconView: ComponentHostView var credibilityIconSize: CGSize? let titleExpandedCredibilityIconView: ComponentHostView var titleExpandedCredibilityIconSize: CGSize? + + let titleVerifiedIconView: ComponentHostView + var verifiedIconSize: CGSize? + let titleExpandedVerifiedIconView: ComponentHostView + var titleExpandedVerifiedIconSize: CGSize? + let subtitleNodeContainer: ASDisplayNode let subtitleNodeRawContainer: ASDisplayNode let subtitleNode: MultiScaleTextNode @@ -191,6 +199,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleExpandedCredibilityIconView = ComponentHostView() self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedCredibilityIconView) + self.titleVerifiedIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateRegular)?.view.addSubview(self.titleVerifiedIconView) + + self.titleExpandedVerifiedIconView = ComponentHostView() + self.titleNode.stateNode(forKey: TitleNodeStateExpanded)?.view.addSubview(self.titleExpandedVerifiedIconView) + self.subtitleNodeContainer = ASDisplayNode() self.subtitleNodeRawContainer = ASDisplayNode() self.subtitleNode = MultiScaleTextNode(stateKeys: [TitleNodeStateRegular, TitleNodeStateExpanded]) @@ -434,9 +448,15 @@ final class PeerInfoHeaderNode: ASDisplayNode { } private var currentCredibilityIcon: CredibilityIcon? + private var currentVerifiedIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { + var threadData = threadData + if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + threadData = nil + } + self.state = state self.peer = peer self.threadData = threadData @@ -468,12 +488,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let credibilityIcon: CredibilityIcon + var verifiedIcon: CredibilityIcon = .none if let peer = peer { if peer.isFake { credibilityIcon = .fake } else if peer.isScam { credibilityIcon = .scam - } else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { + if peer is TelegramChannel, peer.isVerified { + verifiedIcon = .verified + } credibilityIcon = .emojiStatus(emojiStatus) } else if peer.isVerified { credibilityIcon = .verified @@ -556,6 +580,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let navigationContentsAccentColor: UIColor let navigationContentsPrimaryColor: UIColor let navigationContentsSecondaryColor: UIColor + let navigationContentsCanBeExpanded: Bool let contentButtonBackgroundColor: UIColor let contentButtonForegroundColor: UIColor @@ -618,8 +643,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isSettings { backgroundTransitionDistance -= 100.0 } - let contentOffset = max(0.0, contentOffset - backgroundTransitionDistance) - innerBackgroundTransitionFraction = max(0.0, min(1.0, contentOffset / backgroundTransitionStepDistance)) + if isMediaOnly { + innerBackgroundTransitionFraction = 1.0 + } else { + let contentOffset = max(0.0, contentOffset - backgroundTransitionDistance) + innerBackgroundTransitionFraction = max(0.0, min(1.0, contentOffset / backgroundTransitionStepDistance)) + } self.expandedBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.mixedWith(headerBackgroundColor, alpha: 1.0 - innerBackgroundTransitionFraction), forceKeepBlur: true, transition: transition) @@ -643,6 +672,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationContentsAccentColor = collapsedHeaderNavigationContentsAccentColor navigationContentsPrimaryColor = collapsedHeaderNavigationContentsPrimaryColor navigationContentsSecondaryColor = collapsedHeaderNavigationContentsSecondaryColor + navigationContentsCanBeExpanded = true + contentButtonBackgroundColor = collapsedHeaderContentButtonBackgroundColor contentButtonForegroundColor = collapsedHeaderContentButtonForegroundColor @@ -654,6 +685,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { contentButtonBackgroundColor = expandedAvatarContentButtonBackgroundColor contentButtonForegroundColor = expandedAvatarContentButtonForegroundColor + navigationContentsCanBeExpanded = false + headerButtonBackgroundColor = expandedAvatarHeaderButtonBackgroundColor } else { let effectiveTransitionFraction: CGFloat = innerBackgroundTransitionFraction < 0.5 ? 0.0 : 1.0 @@ -662,6 +695,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationContentsPrimaryColor = regularNavigationContentsPrimaryColor.mixedWith(collapsedHeaderNavigationContentsPrimaryColor, alpha: effectiveTransitionFraction) navigationContentsSecondaryColor = regularNavigationContentsSecondaryColor.mixedWith(collapsedHeaderNavigationContentsSecondaryColor, alpha: effectiveTransitionFraction) + if peer?.profileColor != nil { + navigationContentsCanBeExpanded = effectiveTransitionFraction == 1.0 + } else { + navigationContentsCanBeExpanded = true + } + contentButtonBackgroundColor = regularContentButtonBackgroundColor//.mixedWith(collapsedHeaderContentButtonBackgroundColor, alpha: effectiveTransitionFraction) contentButtonForegroundColor = regularContentButtonForegroundColor//.mixedWith(collapsedHeaderContentButtonForegroundColor, alpha: effectiveTransitionFraction) @@ -696,8 +735,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: navigationContentsAccentColor, themeColor: navigationContentsAccentColor, loopMode: .forever) } - //let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty - let iconSize = self.titleCredibilityIconView.update( transition: Transition(navigationTransition), component: AnyComponent(EmojiStatusComponent( @@ -778,7 +815,74 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.titleExpandedCredibilityIconSize = expandedIconSize } - self.navigationButtonContainer.updateContentsColor(backgroundContentColor: headerButtonBackgroundColor, contentsColor: navigationContentsAccentColor, transition: navigationTransition) + do { + self.currentVerifiedIcon = verifiedIcon + + var currentEmojiStatus: PeerEmojiStatus? + let emojiRegularStatusContent: EmojiStatusComponent.Content + let emojiExpandedStatusContent: EmojiStatusComponent.Content + switch verifiedIcon { + case .none: + emojiRegularStatusContent = .none + emojiExpandedStatusContent = .none + case .premium: + emojiRegularStatusContent = .premium(color: navigationContentsAccentColor) + emojiExpandedStatusContent = .premium(color: navigationContentsAccentColor) + case .verified: + emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + emojiExpandedStatusContent = .verified(fillColor: navigationContentsAccentColor, foregroundColor: .clear, sizeType: .large) + case .fake: + emojiRegularStatusContent = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_FakeAccount.uppercased()) + emojiExpandedStatusContent = emojiRegularStatusContent + case .scam: + emojiRegularStatusContent = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_ScamAccount.uppercased()) + emojiExpandedStatusContent = emojiRegularStatusContent + case let .emojiStatus(emojiStatus): + currentEmojiStatus = emojiStatus + emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor, themeColor: navigationContentsAccentColor, loopMode: .forever) + emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: navigationContentsAccentColor, themeColor: navigationContentsAccentColor, loopMode: .forever) + } + + let iconSize = self.titleVerifiedIconView.update( + transition: Transition(navigationTransition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiRegularStatusContent, + isVisibleForAnimations: true, + useSharedAnimation: true, + action: nil, + emojiFileUpdated: nil + )), + environment: {}, + containerSize: CGSize(width: 34.0, height: 34.0) + ) + let expandedIconSize = self.titleExpandedVerifiedIconView.update( + transition: Transition(navigationTransition), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.animationCache, + animationRenderer: self.animationRenderer, + content: emojiExpandedStatusContent, + isVisibleForAnimations: true, + useSharedAnimation: true, + action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayPremiumIntro?(strongSelf.titleExpandedVerifiedIconView, currentEmojiStatus, strongSelf.emojiStatusFileAndPackTitle.get(), true) + } + )), + environment: {}, + containerSize: CGSize(width: 34.0, height: 34.0) + ) + + self.verifiedIconSize = iconSize + self.titleExpandedVerifiedIconSize = expandedIconSize + } + + self.navigationButtonContainer.updateContentsColor(backgroundContentColor: headerButtonBackgroundColor, contentsColor: navigationContentsAccentColor, canBeExpanded: navigationContentsCanBeExpanded, transition: navigationTransition) self.titleNode.updateTintColor(color: navigationContentsPrimaryColor, transition: navigationTransition) self.subtitleNode.updateTintColor(color: navigationContentsSecondaryColor, transition: navigationTransition) @@ -819,7 +923,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.navigationBackgroundBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor self.navigationSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - let navigationSeparatorAlpha: CGFloat = state.isEditing && self.isSettings ? min(1.0, contentOffset / (navigationHeight * 0.5)) : 0.0 + let navigationSeparatorAlpha: CGFloat + if isMediaOnly { + navigationSeparatorAlpha = 0.0 + } else { + navigationSeparatorAlpha = state.isEditing && self.isSettings ? min(1.0, contentOffset / (navigationHeight * 0.5)) : 0.0 + } transition.updateAlpha(node: self.navigationBackgroundBackgroundNode, alpha: 1.0 - navigationSeparatorAlpha) transition.updateAlpha(node: self.navigationSeparatorNode, alpha: navigationSeparatorAlpha) @@ -862,8 +971,6 @@ final class PeerInfoHeaderNode: ASDisplayNode { var title: String if peer.id == self.context.account.peerId && !self.isSettings { title = presentationData.strings.Conversation_SavedMessages - } else if peer.id == self.context.account.peerId && !self.isSettings { - title = presentationData.strings.DialogList_Replies } else if let threadData = threadData { title = threadData.info.title } else { @@ -959,6 +1066,17 @@ final class PeerInfoHeaderNode: ASDisplayNode { smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white, shadowColor: titleShadowColor) usernameString = ("", MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white)) + + let (maybePanelStatusData, _, _) = panelStatusData + if let panelStatusData = maybePanelStatusData { + let subtitleColor: UIColor + if panelStatusData.isActivity { + subtitleColor = UIColor.white + } else { + subtitleColor = UIColor.white + } + panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) + } } } else { titleStringText = " " @@ -1105,16 +1223,35 @@ final class PeerInfoHeaderNode: ASDisplayNode { let usernameSize = usernameNodeLayout[TitleNodeStateRegular]!.size var titleHorizontalOffset: CGFloat = 0.0 + var nextIconX: CGFloat = titleSize.width + var nextExpandedIconX: CGFloat = titleExpandedSize.width + if let credibilityIconSize = self.credibilityIconSize, let titleExpandedCredibilityIconSize = self.titleExpandedCredibilityIconSize { - titleHorizontalOffset = -(credibilityIconSize.width + 4.0) / 2.0 + titleHorizontalOffset += -(credibilityIconSize.width + 4.0) / 2.0 + + var collapsedTransitionOffset: CGFloat = 0.0 + if let navigationTransition = self.navigationTransition { + collapsedTransitionOffset = -10.0 * navigationTransition.fraction + } + + transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: nextIconX + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) + nextIconX += 4.0 + credibilityIconSize.width + transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: nextExpandedIconX + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) + nextExpandedIconX += 4.0 + titleExpandedCredibilityIconSize.width + } + + if let verifiedIconSize = self.verifiedIconSize, let titleExpandedVerifiedIconSize = self.titleExpandedVerifiedIconSize { + titleHorizontalOffset += -(verifiedIconSize.width + 4.0) / 2.0 var collapsedTransitionOffset: CGFloat = 0.0 if let navigationTransition = self.navigationTransition { collapsedTransitionOffset = -10.0 * navigationTransition.fraction } - transition.updateFrame(view: self.titleCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)) - transition.updateFrame(view: self.titleExpandedCredibilityIconView, frame: CGRect(origin: CGPoint(x: titleExpandedSize.width + 4.0, y: floor((titleExpandedSize.height - titleExpandedCredibilityIconSize.height) / 2.0) + 1.0), size: titleExpandedCredibilityIconSize)) + transition.updateFrame(view: self.titleVerifiedIconView, frame: CGRect(origin: CGPoint(x: nextIconX + 4.0 + collapsedTransitionOffset, y: floor((titleSize.height - verifiedIconSize.height) / 2.0)), size: verifiedIconSize)) + nextIconX += 4.0 + verifiedIconSize.width + transition.updateFrame(view: self.titleExpandedVerifiedIconView, frame: CGRect(origin: CGPoint(x: nextExpandedIconX + 4.0, y: floor((titleExpandedSize.height - titleExpandedVerifiedIconSize.height) / 2.0) + 1.0), size: titleExpandedVerifiedIconSize)) + nextExpandedIconX += 4.0 + titleExpandedVerifiedIconSize.width } var titleFrame: CGRect @@ -1481,7 +1618,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { if self.isSettings { expandablePart += 20.0 } else { - if peer?.id == self.context.account.peerId { + if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + expandablePart = 0.0 + } else if peer?.id == self.context.account.peerId { expandablePart = 0.0 } else { expandablePart += 99.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 6600a16ca00..198fc3cfba2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -10,6 +10,8 @@ import AccountContext import ContextUI import ChatControllerInteraction import PeerInfoVisualMediaPaneNode +import PeerInfoPaneNode +import PeerInfoChatListPaneNode final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey @@ -418,6 +420,8 @@ private final class PeerInfoPendingPane { } case .recommended: paneNode = PeerInfoRecommendedChannelsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) + case .savedMessagesChats: + paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) } paneNode.parentController = parentController self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) @@ -996,6 +1000,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat title = presentationData.strings.PeerInfo_PaneMembers case .recommended: title = presentationData.strings.PeerInfo_PaneRecommended + case .savedMessagesChats: + title = presentationData.strings.DialogList_TabTitle } return PeerInfoPaneSpecifier(key: key, title: title) }, selectedPane: self.currentPaneKey, transitionFraction: self.transitionFraction, transition: transition) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 93abdc8731c..24db27103f0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -110,6 +110,10 @@ import PeerNameColorScreen import PeerAllowedReactionsScreen import ChatMessageSelectionInputPanelNode import ChatHistorySearchContainerNode +import PeerInfoPaneNode +import MediaPickerUI +import AttachmentUI +import BoostLevelIconComponent public enum PeerInfoAvatarEditingMode { case generic @@ -504,6 +508,7 @@ private enum PeerInfoSettingsSection { case language case stickers case premium + case premiumGift case passport case watch case support @@ -978,6 +983,13 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) + + // MARK: Nicegram, comment this item (hide "Gift Premium") + /* + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + interaction.openSettings(.premiumGift) + })) + */ } /*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: "Payment Method", icon: PresentationResourcesSettings.language, action: { @@ -1328,7 +1340,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese var threadId: Int64 = 0 if case let .replyThread(message) = chatLocation { - threadId = Int64(message.messageId.id) + threadId = message.threadId } let linkText = "https://t.me/\(mainUsername)/\(threadId)" @@ -1702,19 +1714,19 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL switch channel.info { case .broadcast: let ItemUsername = 1 - let ItemNameColor = 2 - let ItemInviteLinks = 3 - let ItemDiscussionGroup = 4 - let ItemSignMessages = 5 - let ItemSignMessagesHelp = 6 - let ItemDeleteChannel = 7 - let ItemReactions = 8 - let ItemAdmins = 9 - let ItemMembers = 10 - let ItemMemberRequests = 11 - let ItemStats = 12 - let ItemBanned = 13 - let ItemRecentActions = 14 + let ItemPeerColor = 2 + let ItemInviteLinks = 4 + let ItemDiscussionGroup = 5 + let ItemSignMessages = 6 + let ItemSignMessagesHelp = 7 + let ItemDeleteChannel = 8 + let ItemReactions = 9 + let ItemAdmins = 10 + let ItemMembers = 11 + let ItemMemberRequests = 12 + let ItemStats = 13 + let ItemBanned = 14 + let ItemRecentActions = 15 let isCreator = channel.flags.contains(.isCreator) @@ -1787,8 +1799,23 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL } if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { - let colors = context.peerNameColors.get(data.peer?.nameColor ?? .blue, dark: presentationData.theme.overallDarkAppearance) - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, colors.main), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { + var colors: [PeerNameColors.Colors] = [] + if let nameColor = channel.nameColor.flatMap({ context.peerNameColors.get($0, dark: presentationData.theme.overallDarkAppearance) }) { + colors.append(nameColor) + } + if let profileColor = channel.profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: presentationData.theme.overallDarkAppearance, subject: .palette) }) { + colors.append(profileColor) + } + let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors) + + var boostIcon: UIImage? + var additionalBadge: String? + if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel < 1 { + boostIcon = generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Info_BoostLevelPlusBadge("1").string) + } else { + additionalBadge = presentationData.strings.Settings_New + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeLabel: additionalBadge, additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) } @@ -2368,6 +2395,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private var translationState: ChatTranslationState? private var translationStateDisposable: Disposable? + private var boostStatus: ChannelBoostStatus? + private var boostStatusDisposable: Disposable? + private var expiringStoryList: PeerExpiringStoryListContext? private var expiringStoryListState: PeerExpiringStoryListContext.State? private var expiringStoryListDisposable: Disposable? @@ -2404,7 +2434,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var forumTopicThreadId: Int64? if case let .replyThread(message) = chatLocation { - forumTopicThreadId = Int64(message.messageId.id) + forumTopicThreadId = message.threadId } self.headerNode = PeerInfoHeaderNode(context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation) self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey) @@ -3105,7 +3135,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in - }, openRequestedPeerSelection: { _, _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in @@ -3458,7 +3488,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.dismiss() case .edit: if case let .replyThread(message) = strongSelf.chatLocation { - let threadId = Int64(message.messageId.id) + let threadId = message.threadId if let threadData = strongSelf.data?.threadData { let controller = ForumCreateTopicScreen(context: strongSelf.context, peerId: strongSelf.peerId, mode: .edit(threadId: threadId, threadInfo: threadData.info, isHidden: threadData.isHidden)) controller.navigationPresentation = .modal @@ -3944,7 +3974,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var currentSelectedFileId: Int64? var topStatusTitle = strongSelf.presentationData.strings.PeerStatusSetup_NoTimerTitle if let peer = strongSelf.data?.peer { - if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { + if let emojiStatus = peer.emojiStatus { selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) currentSelectedFileId = emojiStatus.fileId @@ -4168,6 +4198,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) let _ = context.engine.peers.requestRecommendedChannels(peerId: peerId, forceUpdate: true).startStandalone() + + self.boostStatusDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if case let .channel(channel) = peer, (channel.flags.contains(.isCreator) || channel.adminRights != nil) { + return context.engine.peers.getChannelBoostStatus(peerId: peerId) + } else { + return .single(nil) + } + } + |> deliverOnMainQueue).start(next: { [weak self] boostStatus in + guard let self else { + return + } + self.boostStatus = boostStatus + }) } if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudUser { @@ -4175,13 +4220,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) self.expiringStoryList = expiringStoryList self.storyUploadProgressDisposable = ( - combineLatest(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> distinctUntilChanged, - context.engine.messages.allStoriesUploadProgress() - |> map { value -> Float? in - return value[peerId] - } - |> distinctUntilChanged + combineLatest( + queue: Queue.mainQueue(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.messages.allStoriesUploadProgress() + |> map { value -> Float? in + return value[peerId] + } + |> distinctUntilChanged )).startStrict(next: { [weak self] peer, value in guard let self else { return @@ -4272,6 +4319,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.storyUploadProgressDisposable?.dispose() self.updateAvatarDisposable.dispose() self.joinChannelDisposable.dispose() + self.boostStatusDisposable?.dispose() } override func didLoad() { @@ -4698,7 +4746,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let navigationController = self.controller?.navigationController as? NavigationController else { return } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, message: nil, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in guard let strongSelf = self else { return } @@ -5551,7 +5599,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { var pushControllerImpl: ((ViewController) -> Void)? - let controller = PremiumGiftScreen(context: strongSelf.context, peerId: strongSelf.peerId, options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in + let controller = PremiumGiftScreen(context: strongSelf.context, peerIds: [strongSelf.peerId], options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in pushControllerImpl?(c) }, completion: { [weak self] in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { @@ -7361,12 +7409,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if self.peerId == self.context.account.peerId { let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .account) self.controller?.push(controller) - } else { - let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .channel(self.peerId)) - self.controller?.push(controller) + } else if let peer = self.data?.peer, peer is TelegramChannel { + self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus)) } } - + private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) } @@ -8632,7 +8679,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var threadId: Int64? if case let .replyThread(message) = self.chatLocation { - threadId = Int64(message.messageId.id) + threadId = message.threadId } var temporary = false @@ -8760,118 +8807,121 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro navigationController.setViewControllers(updatedControllers, animated: animated) } switch section { - case .avatar: - self.openAvatarForEditing() - case .edit: - self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil) - case .proxy: - self.controller?.push(proxySettingsController(context: self.context)) - case .stories: - push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved)) - case .savedMessages: - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer = peer else { - return - } - if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) - } - }) - case .recentCalls: - push(CallListController(context: context, mode: .navigation)) - case .devices: - let _ = (self.activeSessionsContextAndCount.get() + case .avatar: + self.openAvatarForEditing() + case .edit: + self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil) + case .proxy: + self.controller?.push(proxySettingsController(context: self.context)) + case .stories: + push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved)) + case .savedMessages: + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer = peer else { + return + } + if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + } + }) + case .recentCalls: + push(CallListController(context: context, mode: .navigation)) + case .devices: + let _ = (self.activeSessionsContextAndCount.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] activeSessionsContextAndCount in + if let strongSelf = self, let activeSessionsContextAndCount = activeSessionsContextAndCount { + let (activeSessionsContext, _, webSessionsContext) = activeSessionsContextAndCount + push(recentSessionsController(context: strongSelf.context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false)) + } + }) + case .chatFolders: + push(chatListFilterPresetListController(context: self.context, mode: .default)) + case .notificationsAndSounds: + if let settings = self.data?.globalSettings { + push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions)) + } + case .privacyAndSecurity: + if let settings = self.data?.globalSettings { + let _ = (combineLatest(self.blockedPeers.get(), self.hasTwoStepAuth.get()) |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] activeSessionsContextAndCount in - if let strongSelf = self, let activeSessionsContextAndCount = activeSessionsContextAndCount { - let (activeSessionsContext, _, webSessionsContext) = activeSessionsContextAndCount - push(recentSessionsController(context: strongSelf.context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false)) - } - }) - case .chatFolders: - push(chatListFilterPresetListController(context: self.context, mode: .default)) - case .notificationsAndSounds: - if let settings = self.data?.globalSettings { - push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions)) - } - case .privacyAndSecurity: - if let settings = self.data?.globalSettings { - let _ = (combineLatest(self.blockedPeers.get(), self.hasTwoStepAuth.get()) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] blockedPeersContext, hasTwoStepAuth in - if let strongSelf = self { - let loginEmailPattern = strongSelf.twoStepAuthData.get() |> map { data -> String? in - return data?.loginEmailPattern + |> deliverOnMainQueue).startStandalone(next: { [weak self] blockedPeersContext, hasTwoStepAuth in + if let strongSelf = self { + let loginEmailPattern = strongSelf.twoStepAuthData.get() |> map { data -> String? in + return data?.loginEmailPattern + } + push(privacyAndSecurityController(context: strongSelf.context, initialSettings: settings.privacySettings, updatedSettings: { [weak self] settings in + self?.privacySettings.set(.single(settings)) + }, updatedBlockedPeers: { [weak self] blockedPeersContext in + self?.blockedPeers.set(.single(blockedPeersContext)) + }, updatedHasTwoStepAuth: { [weak self] hasTwoStepAuthValue in + self?.hasTwoStepAuth.set(.single(hasTwoStepAuthValue)) + }, focusOnItemTag: nil, activeSessionsContext: settings.activeSessionsContext, webSessionsContext: settings.webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth, loginEmailPattern: loginEmailPattern, updatedTwoStepAuthData: { [weak self] in + if let strongSelf = self { + strongSelf.twoStepAuthData.set( + strongSelf.context.engine.auth.twoStepAuthData() + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ) } - push(privacyAndSecurityController(context: strongSelf.context, initialSettings: settings.privacySettings, updatedSettings: { [weak self] settings in - self?.privacySettings.set(.single(settings)) - }, updatedBlockedPeers: { [weak self] blockedPeersContext in - self?.blockedPeers.set(.single(blockedPeersContext)) - }, updatedHasTwoStepAuth: { [weak self] hasTwoStepAuthValue in - self?.hasTwoStepAuth.set(.single(hasTwoStepAuthValue)) - }, focusOnItemTag: nil, activeSessionsContext: settings.activeSessionsContext, webSessionsContext: settings.webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth, loginEmailPattern: loginEmailPattern, updatedTwoStepAuthData: { [weak self] in - if let strongSelf = self { - strongSelf.twoStepAuthData.set( - strongSelf.context.engine.auth.twoStepAuthData() - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - ) - } - }, requestPublicPhotoSetup: { [weak self] completion in - if let strongSelf = self { - strongSelf.openAvatarForEditing(mode: .fallback, completion: completion) - } - }, requestPublicPhotoRemove: { [weak self] completion in - if let strongSelf = self { - strongSelf.openAvatarRemoval(mode: .fallback, completion: completion) - } - })) - } - }) - } - case .passwordSetup: - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in - guard let self else { - return + }, requestPublicPhotoSetup: { [weak self] completion in + if let strongSelf = self { + strongSelf.openAvatarForEditing(mode: .fallback, completion: completion) + } + }, requestPublicPhotoRemove: { [weak self] completion in + if let strongSelf = self { + strongSelf.openAvatarRemoval(mode: .fallback, completion: completion) + } + })) } - let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).startStandalone() }) - - let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) - push(controller) - case .dataAndStorage: - push(dataAndStorageController(context: self.context)) - case .appearance: - push(themeSettingsController(context: self.context)) - case .language: - push(LocalizationListController(context: self.context)) - case .premium: - self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) - case .stickers: - if let settings = self.data?.globalSettings { - push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in - self?.archivedPacks.set(.single(packs)) - })) - } - case .passport: - self.controller?.push(SecureIdAuthController(context: self.context, mode: .list)) - case .watch: - push(watchSettingsController(context: self.context)) - case .support: - // MARK: Nicegram GermanSupport - if self.presentationData.strings.baseLanguageCode.lowercased().contains("de"), - let url = URL(string: "https://t.me/EinleitungHilfeTelegram") { - CoreContainer.shared.urlOpener().open(url) - break + } + case .passwordSetup: + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in + guard let self else { + return } - // - let supportPeer = Promise() - supportPeer.set(context.engine.peers.supportPeerId()) - - self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Settings_FAQ_Intro, actions: [ + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .setupPassword).startStandalone() + }) + + let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context) + push(controller) + case .dataAndStorage: + push(dataAndStorageController(context: self.context)) + case .appearance: + push(themeSettingsController(context: self.context)) + case .language: + push(LocalizationListController(context: self.context)) + case .premium: + self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) + case .premiumGift: + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context) + self.controller?.push(controller) + case .stickers: + if let settings = self.data?.globalSettings { + push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in + self?.archivedPacks.set(.single(packs)) + })) + } + case .passport: + self.controller?.push(SecureIdAuthController(context: self.context, mode: .list)) + case .watch: + push(watchSettingsController(context: self.context)) + case .support: + // MARK: Nicegram GermanSupport + if self.presentationData.strings.baseLanguageCode.lowercased().contains("de"), + let url = URL(string: "https://t.me/EinleitungHilfeTelegram") { + CoreContainer.shared.urlOpener().open(url) + break + } + // + let supportPeer = Promise() + supportPeer.set(context.engine.peers.supportPeerId()) + + self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Settings_FAQ_Intro, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { [weak self] in self?.openFaq() }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in @@ -8884,61 +8934,70 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } })) })]), in: .window(.root)) - case .faq: - self.openFaq() - case .tips: - self.openTips() - case .phoneNumber: - if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { - let introController = PrivacyIntroController(context: self.context, mode: .changePhoneNumber(phoneNumber), proceedAction: { [weak self] in - if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - navigationController.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true) - } - }) - push(introController) - } - case .username: - push(usernameSetupController(context: self.context)) - case .addAccount: - let _ = (activeAccountsAndPeers(context: context) - |> take(1) - |> deliverOnMainQueue - ).startStandalone(next: { [weak self] accountAndPeer, accountsAndPeers in - guard let strongSelf = self else { - return + case .faq: + self.openFaq() + case .tips: + self.openTips() + case .phoneNumber: + if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { + let introController = PrivacyIntroController(context: self.context, mode: .changePhoneNumber(phoneNumber), proceedAction: { [weak self] in + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + navigationController.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true) } - // MARK: Nicegram MaxAccounts - let maximumAvailableAccounts: Int = nicegramMaximumNumberOfAccounts - var count: Int = 1 - for (accountContext, _, _) in accountsAndPeers { - if !accountContext.account.testingEnvironment { - count += 1 + }) + push(introController) + } + case .username: + push(usernameSetupController(context: self.context)) + case .addAccount: + let _ = (activeAccountsAndPeers(context: context) + |> take(1) + |> deliverOnMainQueue + ).startStandalone(next: { [weak self] accountAndPeer, accountsAndPeers in + guard let strongSelf = self else { + return + } + var maximumAvailableAccounts: Int = 3 + if accountAndPeer?.1.isPremium == true && !strongSelf.context.account.testingEnvironment { + maximumAvailableAccounts = 4 + } + var count: Int = 1 + for (accountContext, peer, _) in accountsAndPeers { + if !accountContext.account.testingEnvironment { + if peer.isPremium { + maximumAvailableAccounts = 4 } + count += 1 } - - if count >= maximumAvailableAccounts { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: { - let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts) - replaceImpl?(controller) - return true - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { - navigationController.pushViewController(controller) - } - } else { - strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment) + } + + // MARK: Nicegram MaxAccounts + maximumAvailableAccounts = nicegramMaximumNumberOfAccounts + // + + if count >= maximumAvailableAccounts { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } - }) - case .logout: - if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { - if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { - self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber)) + if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(controller) } + } else { + strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment) } + }) + case .logout: + if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { + if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { + self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber)) + } + } case .nicegram: // MARK: Nicegram DB Changes var accountsContexts: [(AccountContext, EnginePeer)] = [] @@ -8961,20 +9020,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } // case .rememberPassword: - let context = self.context - let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword(doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in - }, presentation: .modalInLargeLayout) - controller.twoStepAuthSettingsController = { configuration in - return twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: .single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: configuration, password: nil))))) - } - controller.passwordRemembered = { - let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).startStandalone() - } - push(controller) - case .emojiStatus: - self.headerNode.invokeDisplayPremiumIntro() - case .powerSaving: - push(energySavingSettingsScreen(context: self.context)) + let context = self.context + let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword(doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in + }, presentation: .modalInLargeLayout) + controller.twoStepAuthSettingsController = { configuration in + return twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: .single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: configuration, password: nil))))) + } + controller.passwordRemembered = { + let _ = dismissServerProvidedSuggestion(account: context.account, suggestion: .validatePassword).startStandalone() + } + push(controller) + case .emojiStatus: + self.headerNode.invokeDisplayPremiumIntro() + case .powerSaving: + push(energySavingSettingsScreen(context: self.context)) } } @@ -9862,7 +9921,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: transition.isAnimated) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: transition.isAnimated) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) if additive { transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) @@ -10225,7 +10284,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let headerInset = sectionInset - let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: animateHeader) + let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: additive, animateHeader: animateHeader) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -11109,7 +11168,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc let navigateChatLocation: NavigateToChatControllerParams.Location if let threadId = item.threadId { navigateChatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: item.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + messageId: MessageId(peerId: item.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )) } else { navigateChatLocation = .peer(itemPeer) @@ -11541,7 +11600,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } let headerInset = sectionInset - topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: true) + topHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, isModalOverlay: layout.isModalOverlay, isMediaOnly: false, contentOffset: 0.0, paneContainerY: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.savedMessagesPeer ?? self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, threadData: self.screenNode.data?.threadData, peerNotificationSettings: self.screenNode.data?.peerNotificationSettings, threadNotificationSettings: self.screenNode.data?.threadNotificationSettings, globalNotificationSettings: self.screenNode.data?.globalNotificationSettings, statusData: self.screenNode.data?.status, panelStatusData: (nil, nil, nil), isSecretChat: self.screenNode.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.screenNode.data?.isContact ?? false, isSettings: self.screenNode.isSettings, state: self.screenNode.state, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: transition, additive: false, animateHeader: true) } let titleScale = (fraction * previousTitleNode.view.bounds.height + (1.0 - fraction) * self.headerNode.titleNodeRawContainer.bounds.height) / previousTitleNode.view.bounds.height @@ -12305,7 +12364,7 @@ private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustom self.avatarNode.setPeer(context: self.item.context, account: self.item.account, theme: self.presentationData.theme, peer: self.item.peer) - if case let .user(user) = self.item.peer, let _ = user.emojiStatus { + if self.item.peer.emojiStatus != nil { rightTextInset += 32.0 } @@ -12323,6 +12382,10 @@ private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustom } else if user.isPremium { iconContent = .premium(color: self.presentationData.theme.list.itemAccentColor) } + } else if case let .channel(channel) = self.item.peer { + if let emojiStatus = channel.emojiStatus { + iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) + } } if let iconContent { let emojiStatusSize = self.emojiStatusView.update( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index b7dd22ae251..dfddf9cf1a7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/EmptyStateIndicatorComponent", "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 508c5fa3b88..79821d32741 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -28,6 +28,7 @@ import MediaPickerUI import StoryContainerScreen import EmptyStateIndicatorComponent import UIKitRuntimeUtils +import PeerInfoPaneNode private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 46594cbc522..b068285ea57 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -28,56 +28,7 @@ import InvisibleInkDustNode import MediaPickerUI import ChatControllerInteraction import UIKitRuntimeUtils - -public enum PeerInfoPaneKey: Int32 { - case members - case stories - case media - case files - case music - case voice - case links - case gifs - case groupsInCommon - case recommended -} - -public struct PeerInfoStatusData: Equatable { - public var text: String - public var isActivity: Bool - public var key: PeerInfoPaneKey? - - public init( - text: String, - isActivity: Bool, - key: PeerInfoPaneKey? - ) { - self.text = text - self.isActivity = isActivity - self.key = key - } -} - -public protocol PeerInfoPaneNode: ASDisplayNode { - var isReady: Signal { get } - - var parentController: ViewController? { get set } - - var status: Signal { get } - var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set } - var tabBarOffset: CGFloat { get } - - func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) - func scrollToTop() -> Bool - func transferVelocity(_ velocity: CGFloat) - func cancelPreviewGestures() - func findLoadedMessage(id: MessageId) -> Message? - func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? - func addToTransitionSurface(view: UIView) - func updateHiddenMedia() - func updateSelectedMessages(animated: Bool) - func ensureMessageIsVisible(id: MessageId) -} +import PeerInfoPaneNode private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext @@ -1260,7 +1211,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, var threadId: Int64? if case let .replyThread(message) = chatLocation { - threadId = Int64(message.messageId.id) + threadId = message.threadId } self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType)) @@ -1722,7 +1673,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, var threadId: Int64? if case let .replyThread(message) = chatLocation { - threadId = Int64(message.messageId.id) + threadId = message.threadId } self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType)) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index c58a9ad81d5..78772f9c8a6 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1339,7 +1339,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } } else { - let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false))) + let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false, topPeers: false))) self.contactListNode = contactListNode contactListNode.enableUpdates = true contactListNode.selectionStateUpdated = { [weak self] selectionState in diff --git a/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD new file mode 100644 index 00000000000..b98e6369aed --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PremiumLockButtonSubtitleComponent", + module_name = "PremiumLockButtonSubtitleComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift new file mode 100644 index 00000000000..c9b75a09f86 --- /dev/null +++ b/submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent/Sources/PremiumLockButtonSubtitleComponent.swift @@ -0,0 +1,87 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import BundleIconComponent +import AnimatedTextComponent + +public final class PremiumLockButtonSubtitleComponent: CombinedComponent { + public let count: Int + public let color: UIColor + public let strings: PresentationStrings + + public init(count: Int, color: UIColor, strings: PresentationStrings) { + self.count = count + self.color = color + self.strings = strings + } + + public convenience init(count: Int, theme: PresentationTheme, strings: PresentationStrings) { + self.init(count: count, color: theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), strings: strings) + } + + public static func ==(lhs: PremiumLockButtonSubtitleComponent, rhs: PremiumLockButtonSubtitleComponent) -> Bool { + if lhs.count != rhs.count { + return false + } + if lhs.color !== rhs.color { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + public static var body: Body { + let icon = Child(BundleIconComponent.self) + let text = Child(AnimatedTextComponent.self) + + return { context in + let icon = icon.update( + component: BundleIconComponent( + name: "Chat/Input/Accessory Panels/TextLockIcon", + tintColor: context.component.color, + maxSize: CGSize(width: 10.0, height: 10.0) + ), + availableSize: CGSize(width: 100.0, height: 100.0), + transition: context.transition + ) + var textItems: [AnimatedTextComponent.Item] = [] + + let levelString = context.component.strings.ChannelReactions_LevelRequiredLabel("") + var previousIndex = 0 + let nsLevelString = levelString.string as NSString + for range in levelString.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if range.index == 0 { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(range.index), content: .number(context.component.count, minDigits: 1))) + } + previousIndex = range.range.upperBound + } + if nsLevelString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable(100), content: .text(nsLevelString.substring(with: NSRange(location: previousIndex, length: nsLevelString.length - previousIndex))))) + } + + let text = text.update( + component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.color, items: textItems), + availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0), + transition: context.transition + ) + + let spacing: CGFloat = 3.0 + let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height) + context.add(icon + .position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center) + ) + context.add(text + .position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center) + ) + + return size + } + } +} diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index e9e4eb013bf..2eef28e698a 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -1,3 +1,6 @@ +// MARK: Nicegram RoundedVideos +import NGRoundedVideos +// import Foundation import UIKit import Postbox @@ -867,6 +870,7 @@ private extension MediaEditorValues { additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, + nightTheme: false, drawing: nil, entities: [], toolValues: [:], @@ -984,12 +988,37 @@ private extension MediaEditorValues { qualityPreset = defaultPreset } + // MARK: Nicegram RoundedVideos + let cropRect: CGRect + let cropScale: CGFloat + if qualityPreset == .videoMessage { + let originalCropRect: CGRect + if legacyAdjustments.cropRect.isEmpty { + originalCropRect = CGRect( + origin: .zero, + size: legacyAdjustments.originalSize + ) + } else { + originalCropRect = legacyAdjustments.cropRect + } + + (cropRect, cropScale) = NGRoundedVideos.calcCropRectAndScale( + originalCropRect: originalCropRect + ) + } else { + cropRect = legacyAdjustments.cropRect + cropScale = 1 + } + // + self.init( peerId: EnginePeer.Id(0), originalDimensions: PixelDimensions(legacyAdjustments.originalSize), cropOffset: .zero, - cropRect: legacyAdjustments.cropRect, - cropScale: 1.0, + // MARK: Nicegram RoundedVideos + cropRect: cropRect, + cropScale: cropScale, + // cropRotation: legacyAdjustments.cropRotation, cropMirroring: legacyAdjustments.cropMirrored, cropOrientation: legacyAdjustments.cropOrientation.cropOrientation, @@ -1008,6 +1037,7 @@ private extension MediaEditorValues { additionalVideoTrimRange: nil, additionalVideoOffset: nil, additionalVideoVolume: nil, + nightTheme: false, drawing: drawing, entities: entities, toolValues: toolValues, diff --git a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD new file mode 100644 index 00000000000..8606deae330 --- /dev/null +++ b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SavedMessagesScreen", + module_name = "SavedMessagesScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift new file mode 100644 index 00000000000..9fd1b3bdaba --- /dev/null +++ b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/Sources/SavedMessagesScreen.swift @@ -0,0 +1,86 @@ +import Foundation +import AsyncDisplayKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import TelegramCore +import AccountContext +import SwiftSignalKit +import ViewControllerComponent + +private final class SavedMessagesScreenComponent: Component { + public let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + } + + public static func ==(lhs: SavedMessagesScreenComponent, rhs: SavedMessagesScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + public final class View: UIView { + private var component: SavedMessagesScreenComponent? + private var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + + override public init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = true + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func update(component: SavedMessagesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let themeUpdated = self.environment?.theme !== environment.theme + + self.environment = environment + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class SavedMessagesScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: SavedMessagesScreenComponent(context: context), navigationBarAppearance: .none) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } +} diff --git a/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/BUILD b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/BUILD new file mode 100644 index 00000000000..9419d42c9bc --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BoostLevelIconComponent", + module_name = "BoostLevelIconComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift new file mode 100644 index 00000000000..5f8d9d42d95 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BoostLevelIconComponent/Sources/BoostLevelIconComponent.swift @@ -0,0 +1,105 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData + +public func generateDisclosureActionBoostLevelBadgeImage(text: String) -> UIImage { + let attributedText = NSAttributedString(string: text, attributes: [ + .font: Font.medium(12.0), + .foregroundColor: UIColor.white + ]) + let bounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 4.0 + let size = CGSize(width: leftInset + rightInset + ceil(bounds.width), height: 20.0) + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + + context.resetClip() + + UIGraphicsPushContext(context) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) { + let imageFit: CGFloat = 14.0 + let imageSize = image.size.aspectFitted(CGSize(width: imageFit, height: imageFit)) + let imageRect = CGRect(origin: CGPoint(x: 2.0, y: UIScreenPixel + floorToScreenPixels((size.height - imageSize.height) * 0.5)), size: imageSize) + image.draw(in: imageRect) + } + + attributedText.draw(at: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - bounds.height) * 0.5))) + + UIGraphicsPopContext() + })! +} + +public final class BoostLevelIconComponent: Component { + let strings: PresentationStrings + let level: Int + + public init( + strings: PresentationStrings, + level: Int + ) { + self.strings = strings + self.level = level + } + + public static func ==(lhs: BoostLevelIconComponent, rhs: BoostLevelIconComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.level != rhs.level { + return false + } + return true + } + + public final class View: UIView { + private let imageView: UIImageView + + private var component: BoostLevelIconComponent? + + override init(frame: CGRect) { + self.imageView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component != component { + self.imageView.image = generateDisclosureActionBoostLevelBadgeImage(text: component.strings.Channel_Appearance_BoostLevel("\(component.level)").string) + } + self.component = component + + if let image = self.imageView.image { + self.imageView.frame = CGRect(origin: CGPoint(), size: image.size) + return image.size + } else { + return CGSize(width: 1.0, height: 20.0) + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/GenerateThemeName/BUILD b/submodules/TelegramUI/Components/Settings/GenerateThemeName/BUILD new file mode 100644 index 00000000000..32c10189fc8 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/GenerateThemeName/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GenerateThemeName", + module_name = "GenerateThemeName", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift b/submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateGradientColors.swift similarity index 99% rename from submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift rename to submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateGradientColors.swift index 48d4c93dc9f..d9af1c22bf5 100644 --- a/submodules/SettingsUI/Sources/Themes/GenerateGradientColors.swift +++ b/submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateGradientColors.swift @@ -302,7 +302,7 @@ private let colorPairs: [(UInt32, UInt32)] = [ (0x00416a, 0xe4e5e6) ] -func generateGradientColors(color: UIColor) -> (UIColor, UIColor) { +public func generateGradientColors(color: UIColor) -> (UIColor, UIColor) { var nearest: (colors: (lhs: UInt32, rhs: UInt32), distance: Int32)? for (lhs, rhs) in colorPairs { let lhsDistance = color.distance(to: UIColor(rgb: lhs)) diff --git a/submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift b/submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateThemeName.swift similarity index 98% rename from submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift rename to submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateThemeName.swift index 7c54bdf1f7e..54df9cbfaef 100644 --- a/submodules/SettingsUI/Sources/Themes/GenerateThemeName.swift +++ b/submodules/TelegramUI/Components/Settings/GenerateThemeName/Sources/GenerateThemeName.swift @@ -297,7 +297,7 @@ private let subjectives: [String] = [ "Zone" ] -func generateThemeName(accentColor: UIColor) -> String { +public func generateThemeName(accentColor: UIColor) -> String { var nearest: (color: UInt32, distance: Int32)? for (color, _) in colors { let distance = accentColor.distance(to: UIColor(rgb: color)) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD index dd8d1627dba..7d60b8c35cd 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PresentationDataUtils", "//submodules/UndoUI", "//submodules/WallpaperBackgroundNode", + "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/SolidRoundedButtonNode", @@ -28,6 +29,24 @@ swift_library( "//submodules/PremiumUI", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/AvatarNode", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/Settings/ThemeCarouselItem", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/WallpaperResources", + "//submodules/MediaPickerUI", + "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", + "//submodules/TelegramUI/Components/Settings/WallpaperGridScreen", + "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift new file mode 100644 index 00000000000..ef33770dfc7 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -0,0 +1,1554 @@ +import Foundation +import UIKit +import Photos +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import UndoUI +import EntityKeyboard +import PremiumUI +import ComponentFlow +import BundleIconComponent +import AnimatedTextComponent +import ViewControllerComponent +import ButtonComponent +import PremiumLockButtonSubtitleComponent +import ListItemComponentAdaptor +import ListSectionComponent +import MultilineTextComponent +import ThemeCarouselItem +import ListActionItemComponent +import EmojiStatusSelectionComponent +import EmojiStatusComponent +import DynamicCornerRadiusView +import ComponentDisplayAdapters +import WallpaperResources +import MediaPickerUI +import WallpaperGalleryScreen +import WallpaperGridScreen +import BoostLevelIconComponent + +private final class EmojiActionIconComponent: Component { + let context: AccountContext + let color: UIColor + let fileId: Int64? + let file: TelegramMediaFile? + + init( + context: AccountContext, + color: UIColor, + fileId: Int64?, + file: TelegramMediaFile? + ) { + self.context = context + self.color = color + self.fileId = fileId + self.file = file + } + + static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.fileId != rhs.fileId { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class View: UIView { + private var icon: ComponentView? + + func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let size = CGSize(width: 24.0, height: 24.0) + + if let fileId = component.fileId { + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let _ = icon.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .customEmoji(fileId: fileId), + size: size, + placeholderColor: .lightGray, + themeColor: component.color, + loopMode: .forever + ), + isVisibleForAnimations: false, + action: nil + )), + environment: {}, + containerSize: size + ) + let iconFrame = CGRect(origin: CGPoint(), size: size) + if let iconView = icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } else { + if let icon = self.icon { + self.icon = nil + icon.view?.removeFromSuperview() + } + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class ChannelAppearanceScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peerId: EnginePeer.Id + let boostStatus: ChannelBoostStatus? + + init( + context: AccountContext, + peerId: EnginePeer.Id, + boostStatus: ChannelBoostStatus? + ) { + self.context = context + self.peerId = peerId + self.boostStatus = boostStatus + } + + static func ==(lhs: ChannelAppearanceScreenComponent, rhs: ChannelAppearanceScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + + return true + } + + private final class ContentsData { + let peer: EnginePeer? + let peerWallpaper: TelegramWallpaper? + let subscriberCount: Int? + let availableThemes: [TelegramTheme] + + init(peer: EnginePeer?, peerWallpaper: TelegramWallpaper?, subscriberCount: Int?, availableThemes: [TelegramTheme]) { + self.peer = peer + self.peerWallpaper = peerWallpaper + self.subscriberCount = subscriberCount + self.availableThemes = availableThemes + } + + static func get(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + return combineLatest( + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId), + TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId) + ), + telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) + ) + |> map { peerData, cloudThemes -> ContentsData in + let (peer, subscriberCount, wallpaper) = peerData + return ContentsData( + peer: peer, + peerWallpaper: wallpaper, + subscriberCount: subscriberCount, + availableThemes: cloudThemes + ) + } + } + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct ResolvedState { + struct Changes: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let nameColor = Changes(rawValue: 1 << 0) + static let profileColor = Changes(rawValue: 1 << 1) + static let replyFileId = Changes(rawValue: 1 << 2) + static let backgroundFileId = Changes(rawValue: 1 << 3) + static let emojiStatus = Changes(rawValue: 1 << 4) + static let wallpaper = Changes(rawValue: 1 << 5) + } + + var nameColor: PeerNameColor + var profileColor: PeerNameColor? + var replyFileId: Int64? + var backgroundFileId: Int64? + var emojiStatus: PeerEmojiStatus? + var wallpaper: TelegramWallpaper? + + var changes: Changes + + init(nameColor: PeerNameColor, profileColor: PeerNameColor?, replyFileId: Int64?, backgroundFileId: Int64?, emojiStatus: PeerEmojiStatus?, wallpaper: TelegramWallpaper?, changes: Changes) { + self.nameColor = nameColor + self.profileColor = profileColor + self.replyFileId = replyFileId + self.backgroundFileId = backgroundFileId + self.emojiStatus = emojiStatus + self.wallpaper = wallpaper + self.changes = changes + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollView + private let actionButton = ComponentView() + private let bottomPanelBackgroundView: BlurredBackgroundView + private let bottomPanelSeparator: SimpleLayer + + private let replySection = ComponentView() + private let wallpaperSection = ComponentView() + private let bannerSection = ComponentView() + private let resetColorSection = ComponentView() + private let emojiStatusSection = ComponentView() + + private var chatPreviewItemNode: PeerNameColorChatPreviewItemNode? + + private var isUpdating: Bool = false + + private var component: ChannelAppearanceScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + let isReady = ValuePromise(false, ignoreRepeated: true) + private var contentsData: ContentsData? + private var contentsDataDisposable: Disposable? + + private var cachedIconFiles: [Int64: TelegramMediaFile] = [:] + + private var updatedPeerNameColor: PeerNameColor? + private var updatedPeerNameEmoji: Int64?? + private var updatedPeerProfileColor: PeerNameColor?? + private var updatedPeerProfileEmoji: Int64?? + private var updatedPeerStatus: PeerEmojiStatus?? + private var updatedPeerWallpaper: WallpaperSelectionResult? + private var temporaryPeerWallpaper: TelegramWallpaper? + + private var requiredBoostSubject: BoostSubject? + + private var currentTheme: PresentationThemeReference? + private var resolvedCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper?)? + private var resolvingCurrentTheme: (reference: PresentationThemeReference, isDark: Bool, disposable: Disposable)? + + private var premiumConfiguration: PremiumConfiguration? + private var boostLevel: Int? + private var boostStatus: ChannelBoostStatus? + private var boostStatusDisposable: Disposable? + + private var isApplyingSettings: Bool = false + private var applyDisposable: Disposable? + + private weak var emojiStatusSelectionController: ViewController? + private weak var currentUndoController: UndoOverlayController? + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + self.bottomPanelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.bottomPanelSeparator = SimpleLayer() + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addSubview(self.bottomPanelBackgroundView) + self.layer.addSublayer(self.bottomPanelSeparator) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.contentsDataDisposable?.dispose() + self.applyDisposable?.dispose() + self.boostStatusDisposable?.dispose() + self.resolvingCurrentTheme?.disposable.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component, let resolvedState = self.resolveState() else { + return true + } + if self.isApplyingSettings { + return false + } + + if !resolvedState.changes.isEmpty { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertTitle, text: presentationData.strings.Channel_Appearance_UnsavedChangesAlertText, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertDiscard, action: { [weak self] in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_Appearance_UnsavedChangesAlertApply, action: { [weak self] in + guard let self else { + return + } + self.applySettings() + }) + ]), in: .window(.root)) + + return false + } + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + private func updateScrolling(transition: Transition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + let bottomNavigationAlphaDistance: CGFloat = 16.0 + let bottomNavigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentSize.height - self.scrollView.bounds.maxY) / bottomNavigationAlphaDistance)) + + transition.setAlpha(view: self.bottomPanelBackgroundView, alpha: bottomNavigationAlpha) + transition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomNavigationAlpha) + } + + private func resolveState() -> ResolvedState? { + guard let contentsData = self.contentsData, let peer = contentsData.peer else { + return nil + } + + var changes: ResolvedState.Changes = [] + + let nameColor: PeerNameColor + if let updatedPeerNameColor = self.updatedPeerNameColor { + nameColor = updatedPeerNameColor + } else if let peerNameColor = peer.nameColor { + nameColor = peerNameColor + } else { + nameColor = .blue + } + if nameColor != peer.nameColor { + changes.insert(.nameColor) + } + + let profileColor: PeerNameColor? + if case let .some(value) = self.updatedPeerProfileColor { + profileColor = value + } else if let peerProfileColor = peer.profileColor { + profileColor = peerProfileColor + } else { + profileColor = nil + } + if profileColor != peer.profileColor { + changes.insert(.profileColor) + } + + let replyFileId: Int64? + if case let .some(value) = self.updatedPeerNameEmoji { + replyFileId = value + } else { + replyFileId = peer.backgroundEmojiId + } + if replyFileId != peer.backgroundEmojiId { + changes.insert(.replyFileId) + } + + let backgroundFileId: Int64? + if case let .some(value) = self.updatedPeerProfileEmoji { + backgroundFileId = value + } else { + backgroundFileId = peer.profileBackgroundEmojiId + } + if backgroundFileId != peer.profileBackgroundEmojiId { + changes.insert(.backgroundFileId) + } + + let emojiStatus: PeerEmojiStatus? + if case let .some(value) = self.updatedPeerStatus { + emojiStatus = value + } else { + emojiStatus = peer.emojiStatus + } + if emojiStatus != peer.emojiStatus { + changes.insert(.emojiStatus) + } + + let wallpaper: TelegramWallpaper? + if let updatedPeerWallpaper = self.updatedPeerWallpaper { + switch updatedPeerWallpaper { + case .remove: + wallpaper = nil + case let .emoticon(emoticon): + wallpaper = .emoticon(emoticon) + case .custom: + wallpaper = self.temporaryPeerWallpaper + } + changes.insert(.wallpaper) + } else { + wallpaper = contentsData.peerWallpaper + } + + return ResolvedState( + nameColor: nameColor, + profileColor: profileColor, + replyFileId: replyFileId, + backgroundFileId: backgroundFileId, + emojiStatus: emojiStatus, + wallpaper: wallpaper, + changes: changes + ) + } + + private func applySettings() { + guard let component = self.component, let resolvedState = self.resolveState(), let premiumConfiguration = self.premiumConfiguration, let requiredBoostSubject = self.requiredBoostSubject else { + return + } + if self.isApplyingSettings { + return + } + + let requiredLevel = requiredBoostSubject.requiredLevel(context: component.context, configuration: premiumConfiguration) + if let boostLevel = self.boostLevel, requiredLevel > boostLevel { + self.displayBoostLevels(subject: requiredBoostSubject) + return + } + + if resolvedState.changes.isEmpty { + self.environment?.controller()?.dismiss() + return + } + + self.isApplyingSettings = true + self.state?.updated(transition: .immediate) + + self.applyDisposable?.dispose() + + let statusFileId = resolvedState.emojiStatus?.fileId + + enum ApplyError { + case generic + } + + var signals: [Signal] = [] + if !resolvedState.changes.intersection([.nameColor, .replyFileId]).isEmpty { + signals.append(component.context.engine.peers.updatePeerNameColor(peerId: component.peerId, nameColor: resolvedState.nameColor, backgroundEmojiId: resolvedState.replyFileId) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + } + if !resolvedState.changes.intersection([.profileColor, .backgroundFileId]).isEmpty { + signals.append(component.context.engine.peers.updatePeerProfileColor(peerId: component.peerId, profileColor: resolvedState.profileColor, profileBackgroundEmojiId: resolvedState.backgroundFileId) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + } + if resolvedState.changes.contains(.emojiStatus) { + signals.append(component.context.engine.peers.updatePeerEmojiStatus(peerId: component.peerId, fileId: statusFileId, expirationDate: nil) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + } + if resolvedState.changes.contains(.wallpaper) { + if let updatedPeerWallpaper { + switch updatedPeerWallpaper { + case .remove: + signals.append(component.context.engine.themes.setChatWallpaper(peerId: component.peerId, wallpaper: nil, forBoth: false) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + case let .emoticon(emoticon): + signals.append(component.context.engine.themes.setChatWallpaper(peerId: component.peerId, wallpaper: .emoticon(emoticon), forBoth: false) + |> ignoreValues + |> mapError { _ -> ApplyError in + return .generic + }) + case let .custom(wallpaperEntry, options, editedImage, cropRect, brightness): + uploadCustomPeerWallpaper(context: component.context, wallpaper: wallpaperEntry, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: component.peerId, forBoth: false, completion: {}) + } + } + } + + self.applyDisposable = (combineLatest(signals) + |> deliverOnMainQueue).start(error: { [weak self] _ in + guard let self, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + + self.isApplyingSettings = false + self.state?.updated(transition: .immediate) + }, completed: { [weak self] in + guard let self else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let navigationController: NavigationController? = self.environment?.controller()?.navigationController as? NavigationController + + self.environment?.controller()?.dismiss() + + if let lastController = navigationController?.viewControllers.last as? ViewController { + let tipController = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Channel_Appearance_ToastAppliedText, cancel: nil, destructive: false), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }) + lastController.present(tipController, in: .window(.root)) + } + }) + } + + private func displayBoostLevels(subject: BoostSubject) { + guard let component = self.component else { + return + } + + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let component = self.component, let peer, let status = self.boostStatus else { + return + } + let controller = PremiumBoostLevelsScreen( + context: component.context, + peer: peer, + subject: subject, + status: status, + openStats: { [weak self] in + guard let self else { + return + } + self.openBoostStats() + }, + openGift: { [weak self] in + guard let self, let component = self.component else { + return + } + let controller = createGiveawayController(context: component.context, peerId: component.peerId, subject: .generic) + self.environment?.controller()?.push(controller) + } + ) + self.environment?.controller()?.push(controller) + + HapticFeedback().impact(.light) + }) + } + + private func openBoostStats() { + guard let component = self.component, let boostStatus = self.boostStatus else { + return + } + let statsController = component.context.sharedContext.makeChannelStatsController(context: component.context, updatedPresentationData: nil, peerId: component.peerId, boosts: true, boostStatus: boostStatus) + self.environment?.controller()?.push(statsController) + } + + private func openCustomWallpaperSetup() { + guard let component = self.component, let contentsData = self.contentsData, let peer = contentsData.peer, let premiumConfiguration = self.premiumConfiguration, let boostStatus = self.boostStatus, let resolvedState = self.resolveState() else { + return + } + let level = boostStatus.level + let requiredWallpaperLevel = Int(BoostSubject.wallpaper.requiredLevel(context: component.context, configuration: premiumConfiguration)) + let requiredCustomWallpaperLevel = Int(BoostSubject.customWallpaper.requiredLevel(context: component.context, configuration: premiumConfiguration)) + + let selectedWallpaper = resolvedState.wallpaper + + let controller = ThemeGridController( + context: component.context, + mode: .peer(peer, contentsData.availableThemes, selectedWallpaper, level < requiredWallpaperLevel ? requiredWallpaperLevel : nil, level < requiredCustomWallpaperLevel ? requiredCustomWallpaperLevel : nil) + ) + controller.completion = { [weak self] result in + guard let self, let component = self.component, let contentsData = self.contentsData else { + return + } + self.updatedPeerWallpaper = result + switch result { + case let .emoticon(emoticon): + if let selectedTheme = contentsData.availableThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) { + self.currentTheme = .cloud(PresentationCloudTheme(theme: selectedTheme, resolvedWallpaper: nil, creatorAccountId: nil)) + } + self.temporaryPeerWallpaper = nil + case .remove: + self.currentTheme = nil + self.temporaryPeerWallpaper = nil + case let .custom(wallpaperEntry, options, editedImage, cropRect, brightness): + let _ = (getTemporaryCustomPeerWallpaper(context: component.context, wallpaper: wallpaperEntry, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness) + |> deliverOnMainQueue).startStandalone(next: { [weak self] wallpaper in + self?.temporaryPeerWallpaper = wallpaper + self?.state?.updated(transition: .immediate) + }) + self.currentTheme = nil + } + self.state?.updated(transition: .immediate) + } + self.environment?.controller()?.push(controller) + } + + private enum EmojiSetupSubject { + case reply + case profile + case status + } + + private var previousEmojiSetupTimestamp: Double? + private func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?, subject: EmojiSetupSubject) { + guard let component = self.component, let environment = self.environment else { + return + } + + let currentTimestamp = CACurrentMediaTime() + if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 { + return + } + self.previousEmojiSetupTimestamp = currentTimestamp + + self.emojiStatusSelectionController?.dismiss() + var selectedItems = Set() + if let currentFileId { + selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId)) + } + + let mappedSubject: EmojiPagerContentComponent.Subject + switch subject { + case .reply, .profile: + mappedSubject = .backgroundIcon + case .status: + mappedSubject = .channelStatus + } + + let mappedMode: EmojiStatusSelectionController.Mode + switch subject { + case .status: + mappedMode = .customStatusSelection(completion: { [weak self] result, timestamp in + guard let self else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + switch subject { + case .status: + self.updatedPeerStatus = (result?.fileId.id).flatMap { PeerEmojiStatus(fileId: $0, expirationDate: timestamp) } + default: + break + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + default: + mappedMode = .backgroundSelection(completion: { [weak self] result in + guard let self else { + return + } + if let result { + self.cachedIconFiles[result.fileId.id] = result + } + switch subject { + case .reply: + self.updatedPeerNameEmoji = (result?.fileId.id) + case .profile: + self.updatedPeerProfileEmoji = (result?.fileId.id) + case .status: + self.updatedPeerStatus = (result?.fileId.id).flatMap { PeerEmojiStatus(fileId: $0, expirationDate: nil) } + } + self.state?.updated(transition: .spring(duration: 0.4)) + }) + } + + let controller = EmojiStatusSelectionController( + context: component.context, + mode: mappedMode, + sourceView: sourceView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + isStandalone: false, + subject: mappedSubject, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: component.context.account.peerId, + selectedItems: selectedItems, + topStatusTitle: nil, + backgroundIconColor: color + ), + currentSelection: currentFileId, + color: color, + destinationItemView: { [weak sourceView] in + guard let sourceView else { + return nil + } + return sourceView + } + ) + self.emojiStatusSelectionController = controller + environment.controller()?.present(controller, in: .window(.root)) + } + + func update(component: ChannelAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let premiumConfiguration: PremiumConfiguration + if let current = self.premiumConfiguration { + premiumConfiguration = current + } else { + premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + self.premiumConfiguration = premiumConfiguration + } + + if self.contentsDataDisposable == nil { + self.contentsDataDisposable = (ContentsData.get(context: component.context, peerId: component.peerId) + |> deliverOnMainQueue).start(next: { [weak self] contentsData in + guard let self, let component = self.component else { + return + } + if self.contentsData == nil && self.boostStatus == nil { + if let boostStatus = component.boostStatus { + self.boostStatus = boostStatus + self.boostLevel = boostStatus.level + } else if case let .channel(channel) = contentsData.peer { + self.boostLevel = channel.approximateBoostLevel.flatMap(Int.init) + } + } + if self.contentsData == nil, let peerWallpaper = contentsData.peerWallpaper { + for cloudTheme in contentsData.availableThemes { + if case let .emoticon(emoticon) = peerWallpaper, cloudTheme.emoticon?.strippedEmoji == emoticon.strippedEmoji { + self.currentTheme = .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: cloudTheme.isCreator ? component.context.account.id : nil)) + break + } + } + } + self.contentsData = contentsData + + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + self.isReady.set(true) + }) + } + if self.boostStatusDisposable == nil { + self.boostStatusDisposable = (component.context.engine.peers.getChannelBoostStatus(peerId: component.peerId) + |> deliverOnMainQueue).start(next: { [weak self] boostStatus in + guard let self else { + return + } + self.boostLevel = boostStatus?.level + self.boostStatus = boostStatus + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + guard let contentsData = self.contentsData, var peer = contentsData.peer, let resolvedState = self.resolveState() else { + return availableSize + } + + var requiredBoostSubjects: [BoostSubject] = [.nameColors(colors: resolvedState.nameColor)] + + let replyIconLevel = Int(BoostSubject.nameIcon.requiredLevel(context: component.context, configuration: premiumConfiguration)) + let profileIconLevel = Int(BoostSubject.profileIcon.requiredLevel(context: component.context, configuration: premiumConfiguration)) + let emojiStatusLevel = Int(BoostSubject.emojiStatus.requiredLevel(context: component.context, configuration: premiumConfiguration)) + let themeLevel = Int(BoostSubject.wallpaper.requiredLevel(context: component.context, configuration: premiumConfiguration)) + + let replyFileId = resolvedState.replyFileId + if replyFileId != nil { + requiredBoostSubjects.append(.nameIcon) + } + + let profileColor = resolvedState.profileColor + if profileColor != nil { + requiredBoostSubjects.append(.profileColors) + } + + let backgroundFileId = resolvedState.backgroundFileId + if backgroundFileId != nil { + requiredBoostSubjects.append(.profileIcon) + } + + let emojiStatus = resolvedState.emojiStatus + if emojiStatus != nil { + requiredBoostSubjects.append(.emojiStatus) + } + let statusFileId = emojiStatus?.fileId + + let cloudThemes: [PresentationThemeReference] = contentsData.availableThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? component.context.account.id : nil)) } + let chatThemes = cloudThemes.filter { $0.emoticon != nil } + + if let currentTheme = self.currentTheme, (self.resolvedCurrentTheme?.reference != currentTheme || self.resolvedCurrentTheme?.isDark != environment.theme.overallDarkAppearance), (self.resolvingCurrentTheme?.reference != currentTheme || self.resolvingCurrentTheme?.isDark != environment.theme.overallDarkAppearance) { + self.resolvingCurrentTheme?.disposable.dispose() + + let disposable = MetaDisposable() + self.resolvingCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, disposable) + + var presentationTheme: PresentationTheme? + switch currentTheme { + case .builtin: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: .builtin(environment.theme.overallDarkAppearance ? .night : .dayClassic)) + case let .cloud(cloudTheme): + presentationTheme = makePresentationTheme(cloudTheme: cloudTheme.theme, dark: environment.theme.overallDarkAppearance) + default: + presentationTheme = makePresentationTheme(mediaBox: component.context.sharedContext.accountManager.mediaBox, themeReference: currentTheme) + } + if let presentationTheme { + let resolvedWallpaper: Signal + if let temporaryPeerWallpaper = self.temporaryPeerWallpaper { + resolvedWallpaper = .single(temporaryPeerWallpaper) + } else if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 { + resolvedWallpaper = cachedWallpaper(account: component.context.account, slug: file.slug, settings: file.settings) + |> map { wallpaper -> TelegramWallpaper? in + return wallpaper?.wallpaper + } + } else { + resolvedWallpaper = .single(presentationTheme.chat.defaultWallpaper) + } + disposable.set((resolvedWallpaper + |> deliverOnMainQueue).startStrict(next: { [weak self] resolvedWallpaper in + guard let self, let environment = self.environment else { + return + } + self.resolvedCurrentTheme = (currentTheme, environment.theme.overallDarkAppearance, presentationTheme, resolvedWallpaper) + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + })) + } + } else if self.currentTheme == nil { + self.resolvingCurrentTheme?.disposable.dispose() + self.resolvingCurrentTheme = nil + self.resolvedCurrentTheme = nil + } + + if let wallpaper = resolvedState.wallpaper { + if wallpaper.isEmoticon { + requiredBoostSubjects.append(.wallpaper) + } else { + requiredBoostSubjects.append(.customWallpaper) + } + } + + if case let .user(user) = peer { + peer = .user(user + .withUpdatedNameColor(resolvedState.nameColor) + .withUpdatedProfileColor(profileColor) + .withUpdatedEmojiStatus(emojiStatus) + .withUpdatedBackgroundEmojiId(replyFileId) + .withUpdatedProfileBackgroundEmojiId(backgroundFileId) + ) + } else if case let .channel(channel) = peer { + peer = .channel(channel + .withUpdatedNameColor(resolvedState.nameColor) + .withUpdatedProfileColor(profileColor) + .withUpdatedEmojiStatus(emojiStatus) + .withUpdatedBackgroundEmojiId(replyFileId) + .withUpdatedProfileBackgroundEmojiId(backgroundFileId) + ) + } + + let requiredBoostSubject: BoostSubject + if let maxBoostSubject = requiredBoostSubjects.max(by: { $0.requiredLevel(context: component.context, configuration: premiumConfiguration) < $1.requiredLevel(context: component.context, configuration: premiumConfiguration) }) { + requiredBoostSubject = maxBoostSubject + } else { + requiredBoostSubject = .nameColors(colors: resolvedState.nameColor) + } + self.requiredBoostSubject = requiredBoostSubject + + let topInset: CGFloat = 24.0 + let bottomContentInset: CGFloat = 24.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight + contentHeight += topInset + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: false, + peerId: EnginePeer.Id(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)), + author: peer.compactDisplayTitle, + photo: peer.profileImageRepresentations, + nameColor: resolvedState.nameColor, + backgroundEmojiId: replyFileId, + reply: (peer.compactDisplayTitle, environment.strings.Channel_Appearance_ExampleReplyText), + linkPreview: (environment.strings.Channel_Appearance_ExampleLinkWebsite, environment.strings.Channel_Appearance_ExampleLinkTitle, environment.strings.Channel_Appearance_ExampleLinkText), + text: environment.strings.Channel_Appearance_ExampleText + ) + + var replyLogoContents: [AnyComponentWithIdentity] = [] + replyLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_NameIcon, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if let boostLevel = self.boostLevel, boostLevel < premiumConfiguration.minChannelNameIconLevel { + replyLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: replyIconLevel + )))) + } + + var chatPreviewTheme: PresentationTheme = environment.theme + var chatPreviewWallpaper: TelegramWallpaper = presentationData.chatWallpaper + if let updatedWallpaper = self.updatedPeerWallpaper, case .remove = updatedWallpaper { + } else if let temporaryPeerWallpaper = self.temporaryPeerWallpaper { + chatPreviewWallpaper = temporaryPeerWallpaper + } else if let resolvedCurrentTheme = self.resolvedCurrentTheme { + chatPreviewTheme = resolvedCurrentTheme.theme + if let wallpaper = resolvedCurrentTheme.wallpaper { + chatPreviewWallpaper = wallpaper + } + } else if let initialWallpaper = contentsData.peerWallpaper, !initialWallpaper.isEmoticon { + chatPreviewWallpaper = initialWallpaper + } + + let replySectionSize = self.replySection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_NameColorFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorChatPreviewItem( + context: component.context, + theme: chatPreviewTheme, + componentTheme: chatPreviewTheme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: chatPreviewWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + messageItems: [messageItem] + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + isProfile: false, + currentColor: resolvedState.nameColor, + updated: { [weak self] value in + guard let self else { + return + } + self.updatedPeerNameColor = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(replyLogoContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, + fileId: replyFileId, + file: replyFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: component.context.peerNameColors.get(resolvedState.nameColor, dark: environment.theme.overallDarkAppearance).main, subject: .reply) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let replySectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: replySectionSize) + if let replySectionView = self.replySection.view { + if replySectionView.superview == nil { + self.scrollView.addSubview(replySectionView) + } + transition.setFrame(view: replySectionView, frame: replySectionFrame) + } + contentHeight += replySectionSize.height + + contentHeight += sectionSpacing + + if !chatThemes.isEmpty { + var wallpaperLogoContents: [AnyComponentWithIdentity] = [] + wallpaperLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_Wallpaper, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if let boostLevel = self.boostLevel, boostLevel < premiumConfiguration.minChannelCustomWallpaperLevel { + wallpaperLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: themeLevel + )))) + } + + var currentTheme = self.currentTheme + var selectedWallpaper: TelegramWallpaper? + if currentTheme == nil, let wallpaper = resolvedState.wallpaper, !wallpaper.isEmoticon { + let theme: PresentationThemeReference = .builtin(.day) + currentTheme = theme + selectedWallpaper = wallpaper + } + + let wallpaperSectionSize = self.wallpaperSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_WallpaperFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: ThemeCarouselThemeItem( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sectionId: 0, + themes: chatThemes, + hasNoTheme: true, + animatedEmojiStickers: component.context.animatedEmojiStickers, + themeSpecificAccentColors: [:], + themeSpecificChatWallpapers: [:], + nightMode: environment.theme.overallDarkAppearance, + channelMode: true, + selectedWallpaper: selectedWallpaper, + currentTheme: currentTheme, + updatedTheme: { [weak self] value in + guard let self, value != .builtin(.day) else { + return + } + self.currentTheme = value + self.temporaryPeerWallpaper = nil + if let value { + self.updatedPeerWallpaper = .emoticon(value.emoticon ?? "") + } else { + self.updatedPeerWallpaper = .remove + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + contextAction: nil + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(wallpaperLogoContents, spacing: 6.0)), + icon: nil, + action: { [weak self] view in + guard let self else { + return + } + self.openCustomWallpaperSetup() + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let wallpaperSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: wallpaperSectionSize) + if let wallpaperSectionView = self.wallpaperSection.view { + if wallpaperSectionView.superview == nil { + self.scrollView.addSubview(wallpaperSectionView) + } + transition.setFrame(view: wallpaperSectionView, frame: wallpaperSectionFrame) + } + contentHeight += wallpaperSectionSize.height + contentHeight += sectionSpacing + } + + var profileLogoContents: [AnyComponentWithIdentity] = [] + profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_ProfileIcon, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if let boostLevel = self.boostLevel, boostLevel < premiumConfiguration.minChannelProfileIconLevel { + profileLogoContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: profileIconLevel + )))) + } + + let bannerBackground: ListSectionComponent.Background + if profileColor != nil { + bannerBackground = .range(from: 1, corners: DynamicCornerRadiusView.Corners(minXMinY: 0.0, maxXMinY: 0.0, minXMaxY: 11.0, maxXMaxY: 11.0)) + } else { + bannerBackground = .range(from: 1, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0)) + } + let bannerSectionSize = self.bannerSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + background: bannerBackground, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_ProfileHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_ProfileFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorProfilePreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + peer: peer, + subtitleString: contentsData.subscriberCount.flatMap { environment.strings.Conversation_StatusSubscribers(Int32($0)) }, + files: self.cachedIconFiles, + nameDisplayOrder: presentationData.nameDisplayOrder + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorItem( + theme: environment.theme, + colors: component.context.peerNameColors, + isProfile: true, + currentColor: profileColor, + updated: { [weak self] value in + guard let self else { + return + } + self.updatedPeerProfileColor = value + self.state?.updated(transition: .spring(duration: 0.4)) + }, + sectionId: 0 + ), + params: listItemParams + ))), + AnyComponentWithIdentity(id: 2, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(profileLogoContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: profileColor.flatMap { profileColor in + component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, + fileId: backgroundFileId, + file: backgroundFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.backgroundFileId, color: resolvedState.profileColor.flatMap { + component.context.peerNameColors.getProfile($0, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, subject: .profile) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let bannerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: bannerSectionSize) + if let bannerSectionView = self.bannerSection.view { + if bannerSectionView.superview == nil { + self.scrollView.addSubview(bannerSectionView) + } + transition.setFrame(view: bannerSectionView, frame: bannerSectionFrame) + } + contentHeight += bannerSectionSize.height + contentHeight += sectionSpacing + + var emojiStatusContents: [AnyComponentWithIdentity] = [] + emojiStatusContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_Status, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + if let boostLevel = self.boostLevel, boostLevel < premiumConfiguration.minChannelEmojiStatusLevel { + emojiStatusContents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(BoostLevelIconComponent( + strings: environment.strings, + level: emojiStatusLevel + )))) + } + + let resetColorSectionSize = self.resetColorSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_ResetProfileColor, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 0 + )), + icon: nil, + hasArrow: false, + action: { [weak self] view in + guard let self else { + return + } + + self.updatedPeerProfileColor = .some(nil) + self.updatedPeerProfileEmoji = .some(nil) + self.state?.updated(transition: .spring(duration: 0.4)) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + let displayResetProfileColor = profileColor != nil || backgroundFileId != nil + + let resetColorSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resetColorSectionSize) + if let resetColorSectionView = self.resetColorSection.view { + if resetColorSectionView.superview == nil { + self.scrollView.addSubview(resetColorSectionView) + } + transition.setPosition(view: resetColorSectionView, position: resetColorSectionFrame.center) + transition.setBounds(view: resetColorSectionView, bounds: CGRect(origin: CGPoint(), size: resetColorSectionFrame.size)) + transition.setScale(view: resetColorSectionView, scale: displayResetProfileColor ? 1.0 : 0.001) + transition.setAlpha(view: resetColorSectionView, alpha: displayResetProfileColor ? 1.0 : 0.0) + } + if displayResetProfileColor { + contentHeight += resetColorSectionSize.height + contentHeight += sectionSpacing + } + + let emojiStatusSectionSize = self.emojiStatusSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_Appearance_StatusFooter, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(emojiStatusContents, spacing: 6.0)), + icon: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: environment.theme.list.itemAccentColor, + fileId: statusFileId, + file: statusFileId.flatMap { self.cachedIconFiles[$0] } + ))), + action: { [weak self] view in + guard let self, let resolvedState = self.resolveState(), let view = view as? ListActionItemComponent.View, let iconView = view.iconView else { + return + } + + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.emojiStatus?.fileId, color: nil, subject: .status) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let emojiStatusSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: emojiStatusSectionSize) + if let emojiStatusSectionView = self.emojiStatusSection.view { + if emojiStatusSectionView.superview == nil { + self.scrollView.addSubview(emojiStatusSectionView) + } + transition.setFrame(view: emojiStatusSectionView, frame: emojiStatusSectionFrame) + } + contentHeight += emojiStatusSectionSize.height + + contentHeight += bottomContentInset + + var buttonContents: [AnyComponentWithIdentity] = [] + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.Channel_Appearance_ApplyButton, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + ))) + + let requiredLevel = requiredBoostSubject.requiredLevel(context: component.context, configuration: premiumConfiguration) + if let boostLevel = self.boostLevel, requiredLevel > boostLevel { + buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(PremiumLockButtonSubtitleComponent( + count: Int(requiredLevel), + theme: environment.theme, + strings: environment.strings + )))) + } + + let buttonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + VStack(buttonContents, spacing: 3.0) + )), + isEnabled: true, + tintWhenDisabled: false, + displaysProgress: self.isApplyingSettings, + action: { [weak self] in + guard let self else { + return + } + self.applySettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + contentHeight += buttonSize.height + + contentHeight += bottomInset + contentHeight += environment.safeInsets.bottom + + let buttonY = availableSize.height - bottomInset - environment.safeInsets.bottom - buttonSize.height + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: buttonY), size: buttonSize) + if let buttonView = self.actionButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + transition.setAlpha(view: buttonView, alpha: 1.0) + } + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: buttonY - 8.0), size: CGSize(width: availableSize.width, height: availableSize.height - buttonY + 8.0)) + transition.setFrame(view: self.bottomPanelBackgroundView, frame: bottomPanelFrame) + self.bottomPanelBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.bottomPanelBackgroundView.update(size: bottomPanelFrame.size, transition: transition.containedViewLayoutTransition) + + self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + transition.setFrame(layer: self.bottomPanelSeparator, frame: CGRect(origin: CGPoint(x: bottomPanelFrame.minX, y: bottomPanelFrame.minY), size: CGSize(width: bottomPanelFrame.width, height: UIScreenPixel))) + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: availableSize.height - bottomPanelFrame.minY, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ChannelAppearanceScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didSetReady: Bool = false + + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + peerId: EnginePeer.Id, + boostStatus: ChannelBoostStatus? + ) { + self.context = context + + super.init(context: context, component: ChannelAppearanceScreenComponent( + context: context, + peerId: peerId, + boostStatus: boostStatus + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = presentationData.strings.Channel_Appearance_Title + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.ready.set(.never()) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if let componentView = self.node.hostView.componentView as? ChannelAppearanceScreenComponent.View { + if !self.didSetReady { + self.didSetReady = true + self.ready.set(componentView.isReady.get()) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 920bbc1b469..31335793745 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -11,10 +11,11 @@ import ItemListUI import PresentationDataUtils import AccountContext import WallpaperBackgroundNode +import ListItemComponentAdaptor -final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { +final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { struct MessageItem: Equatable { - static func == (lhs: MessageItem, rhs: MessageItem) -> Bool { + static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool { if lhs.outgoing != rhs.outgoing { return false } @@ -118,6 +119,44 @@ final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.chatBubbleCorners != rhs.chatBubbleCorners { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.messageItems != rhs.messageItems { + return false + } + return true + } } final class PeerNameColorChatPreviewItemNode: ListViewItemNode { @@ -260,7 +299,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) - if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId { + if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId || currentItem.theme !== item.theme || currentItem.wallpaper != item.wallpaper { if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) strongSelf.view.addSubview(snapshot) @@ -329,31 +368,42 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.maskNode, at: 3) } - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + + var hasTopCorners = false + var hasBottomCorners = false + + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : 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)) } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 0.0 - bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) @@ -377,8 +427,6 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate) } strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) - 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/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift index 25c36e87b95..2c7cba50d8a 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift @@ -10,6 +10,7 @@ import MergeLists import ItemListUI import PresentationDataUtils import AccountContext +import ListItemComponentAdaptor private enum PeerNameColorEntryId: Hashable { case color(Int32) @@ -313,7 +314,7 @@ private final class PeerNameColorIconItemNode : ListViewItemNode { } } -final class PeerNameColorItem: ListViewItem, ItemListItem { +final class PeerNameColorItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { var sectionId: ItemListSectionId let theme: PresentationTheme @@ -365,6 +366,27 @@ final class PeerNameColorItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorItem, rhs: PeerNameColorItem) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.colors != rhs.colors { + return false + } + if lhs.isProfile != rhs.isProfile { + return false + } + if lhs.currentColor != rhs.currentColor { + return false + } + + return true + } } private struct PeerNameColorItemNodeTransition { @@ -394,7 +416,7 @@ private func ensureColorVisible(listNode: ListView, color: PeerNameColor, animat } } if let resultNode = resultNode { - listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0) + listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 76.0) return true } else { return false @@ -528,24 +550,30 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - if item.currentColor != nil { - switch neighbors.top { - case .sameSection(false): + if params.isStandalone { + strongSelf.backgroundNode.isHidden = true + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + if item.currentColor != nil { + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + } else { strongSelf.topStripeNode.isHidden = true - default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners } - } else { - strongSelf.topStripeNode.isHidden = true - hasTopCorners = true - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -555,15 +583,17 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } 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.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) 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)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 8.0 diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index 9db8346dd77..e9101135d21 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -14,24 +14,29 @@ import ComponentFlow import PeerInfoCoverComponent import AvatarNode import EmojiStatusComponent +import ListItemComponentAdaptor +import ComponentDisplayAdapters +import MultilineTextComponent -final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { +final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { let context: AccountContext let theme: PresentationTheme let componentTheme: PresentationTheme let strings: PresentationStrings let sectionId: ItemListSectionId let peer: EnginePeer? + let subtitleString: String? let files: [Int64: TelegramMediaFile] let nameDisplayOrder: PresentationPersonNameOrder - init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, peer: EnginePeer?, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { self.context = context self.theme = theme self.componentTheme = componentTheme self.strings = strings self.sectionId = sectionId self.peer = peer + self.subtitleString = subtitleString self.files = files self.nameDisplayOrder = nameDisplayOrder } @@ -46,7 +51,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply() }) + return (nil, { _ in apply(.None) }) }) } } @@ -61,13 +66,43 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in - apply() + apply(animation) }) } } } } } + + func item() -> ListViewItem { + return self + } + + static func ==(lhs: PeerNameColorProfilePreviewItem, rhs: PeerNameColorProfilePreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.files != rhs.files { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + + return true + } } final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { @@ -104,7 +139,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { deinit { } - func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { return { [weak self] item, params, neighbors in let separatorHeight = UIScreenPixel @@ -117,7 +152,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - return (layout, { [weak self] in + return (layout, { [weak self] animation in guard let self else { return } @@ -139,32 +174,41 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { if self.maskNode.supernode == nil { self.addSubnode(self.maskNode) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + self.topStripeNode.isHidden = true + self.bottomStripeNode.isHidden = true + self.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): self.topStripeNode.isHidden = true default: hasTopCorners = true self.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + self.bottomStripeNode.isHidden = item.peer?.profileColor == nil + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + self.bottomStripeNode.isHidden = hasCorners + } + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 0.0 - bottomStripeOffset = -separatorHeight - self.bottomStripeNode.isHidden = item.peer?.profileColor == nil - default: - bottomStripeInset = 0.0 - bottomStripeOffset = 0.0 - hasBottomCorners = true - self.bottomStripeNode.isHidden = hasCorners - } - - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) @@ -217,6 +261,61 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } self.avatarNode.frame = avatarFrame.offsetBy(dx: coverFrame.minX, dy: coverFrame.minY) + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) + + enum CredibilityIcon { + case none + case premium + case verified + case fake + case scam + case emojiStatus(PeerEmojiStatus) + } + + let credibilityIcon: CredibilityIcon + if let peer = item.peer { + if peer.isFake { + credibilityIcon = .fake + } else if peer.isScam { + credibilityIcon = .scam + } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus, !premiumConfiguration.isPremiumDisabled { + credibilityIcon = .emojiStatus(emojiStatus) + } else if case let .channel(channel) = peer, let emojiStatus = channel.emojiStatus, !premiumConfiguration.isPremiumDisabled { + credibilityIcon = .emojiStatus(emojiStatus) + } else if peer.isVerified { + credibilityIcon = .verified + } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != item.context.account.peerId) { + credibilityIcon = .premium + } else { + credibilityIcon = .none + } + } else { + credibilityIcon = .none + } + + let statusColor: UIColor + if let peer = item.peer, peer.profileColor != nil { + statusColor = .white + } else { + statusColor = item.theme.list.itemCheckColors.fillColor + } + + let emojiStatusContent: EmojiStatusComponent.Content + switch credibilityIcon { + case .none: + emojiStatusContent = .none + case .premium: + emojiStatusContent = .premium(color: statusColor) + case .verified: + emojiStatusContent = .verified(fillColor: statusColor, foregroundColor: .clear, sizeType: .large) + case .fake: + emojiStatusContent = .text(color: item.theme.chat.message.incoming.scamColor, string: item.strings.Message_FakeAccount.uppercased()) + case .scam: + emojiStatusContent = .text(color: item.theme.chat.message.incoming.scamColor, string: item.strings.Message_ScamAccount.uppercased()) + case let .emojiStatus(emojiStatus): + emojiStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 80.0, height: 80.0), placeholderColor: item.theme.list.mediaPlaceholderColor, themeColor: statusColor, loopMode: .forever) + } + let backgroundColor: UIColor let titleColor: UIColor let subtitleColor: UIColor @@ -230,24 +329,82 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { backgroundColor = .clear } + var hasStatusIcon = false + if case .none = emojiStatusContent { + } else { + hasStatusIcon = true + } + + var maxTitleWidth = coverFrame.width - 16.0 + if hasStatusIcon { + maxTitleWidth -= 4.0 + 34.0 + } + let titleString: String = item.peer?.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder) ?? " " let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text( - text: titleString, font: Font.semibold(28.0), color: titleColor + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(28.0), textColor: titleColor)), + maximumNumberOfLines: 1 )), environment: {}, - containerSize: CGSize(width: coverFrame.width - 16.0, height: 100.0) + containerSize: CGSize(width: maxTitleWidth, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: coverFrame.minX + floor((coverFrame.width - titleSize.width) * 0.5), y: avatarFrame.maxY + 10.0), size: titleSize) + + var titleContentWidth = titleSize.width + if case .none = emojiStatusContent { + } else { + titleContentWidth += 4.0 + 34.0 + } + + let titleFrame = CGRect(origin: CGPoint(x: coverFrame.minX + floor((coverFrame.width - titleContentWidth) * 0.5), y: avatarFrame.maxY + 10.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) self.view.addSubview(titleView) } - titleView.frame = titleFrame + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + animation.animator.updatePosition(layer: titleView.layer, position: titleFrame.origin, completion: nil) + } + + let icon: ComponentView + if let current = self.icon { + icon = current + } else { + icon = ComponentView() + self.icon = icon + } + let iconSize = CGSize(width: 34.0, height: 34.0) + let _ = icon.update( + transition: Transition(animation.transition), + component: AnyComponent(EmojiStatusComponent( + context: item.context, + animationCache: item.context.animationCache, + animationRenderer: item.context.animationRenderer, + content: emojiStatusContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: iconSize + ) + if let iconView = icon.view { + if iconView.superview == nil { + self.view.addSubview(iconView) + } + let iconFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - iconSize.height) * 0.5)), size: iconSize) + iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + animation.animator.updatePosition(layer: iconView.layer, position: iconFrame.center, completion: nil) } - let subtitleString: String = item.strings.LastSeen_JustNow + let subtitleString: String + if let value = item.subtitleString { + subtitleString = value + } else if case .channel = item.peer { + subtitleString = item.strings.Channel_Status + } else { + subtitleString = item.strings.LastSeen_JustNow + } let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(Text( @@ -265,8 +422,6 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } self.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) - self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - self.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/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 10108d5f3c5..798bcdc873b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -696,12 +696,7 @@ public func PeerNameColorScreen( emojiContent: emojiContent ) - let title: ItemListControllerTitle - if case .user = peer { - title = .sectionControl([presentationData.strings.ProfileColorSetup_TitleName, presentationData.strings.ProfileColorSetup_TitleProfile], state.selectedTabIndex) - } else { - title = .text(presentationData.strings.ProfileColorSetup_TitleChannelColor) - } + let title: ItemListControllerTitle = .sectionControl([presentationData.strings.ProfileColorSetup_TitleName, presentationData.strings.ProfileColorSetup_TitleProfile], state.selectedTabIndex) let controllerState = ItemListControllerState( presentationData: ItemListPresentationData(presentationData), @@ -896,7 +891,7 @@ public func PeerNameColorScreen( } let link = status.url - let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { + let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors(colors: .blue), isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { UIPasteboard.general.string = link let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })) diff --git a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD new file mode 100644 index 00000000000..d6fd0655ff4 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SettingsThemeWallpaperNode", + module_name = "SettingsThemeWallpaperNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/GradientBackground", + "//submodules/WallpaperResources", + "//submodules/StickerResources", + "//submodules/AccountContext", + "//submodules/RadialStatusNode", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift similarity index 73% rename from submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift rename to submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift index c2e06860238..74b0f895a32 100644 --- a/submodules/SettingsUI/Sources/Themes/SettingsThemeWallpaperNode.swift +++ b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift @@ -10,6 +10,9 @@ import AccountContext import RadialStatusNode import WallpaperResources import GradientBackground +import StickerResources +import AnimatedStickerNode +import TelegramAnimatedStickerNode private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { return .single({ arguments in @@ -42,17 +45,24 @@ private let blackColorImage: UIImage? = { return context.generateImage() }() -final class SettingsThemeWallpaperNode: ASDisplayNode { - var wallpaper: TelegramWallpaper? +public final class SettingsThemeWallpaperNode: ASDisplayNode { + public var wallpaper: TelegramWallpaper? private var arguments: PatternWallpaperArguments? - let buttonNode = HighlightTrackingButtonNode() - let backgroundNode = ASImageNode() - let imageNode = TransformImageNode() + private var emojiFile: TelegramMediaFile? + + public let buttonNode = HighlightTrackingButtonNode() + public let backgroundNode = ASImageNode() + public let imageNode = TransformImageNode() private var gradientNode: GradientBackgroundNode? private let statusNode: RadialStatusNode - var pressed: (() -> Void)? + private let emojiContainerNode: ASDisplayNode + private let emojiImageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private let stickerFetchedDisposable = MetaDisposable() + + public var pressed: (() -> Void)? private let displayLoading: Bool private var isSelected: Bool = false @@ -60,7 +70,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { private let isLoadedDisposable = MetaDisposable() - init(displayLoading: Bool = false, overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) { + public init(displayLoading: Bool = false, overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) { self.displayLoading = displayLoading self.imageNode.contentAnimations = [.subsequentUpdates] @@ -69,6 +79,9 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter) self.statusNode.isUserInteractionEnabled = false + self.emojiContainerNode = ASDisplayNode() + self.emojiImageNode = TransformImageNode() + super.init() self.addSubnode(self.backgroundNode) @@ -76,14 +89,37 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.addSubnode(self.buttonNode) self.addSubnode(self.statusNode) + self.addSubnode(self.emojiContainerNode) +// self.emojiContainerNode.addSubnode(self.emojiNode) + self.emojiContainerNode.addSubnode(self.emojiImageNode) + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + var firstTime = true + self.emojiImageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } } deinit { self.isLoadedDisposable.dispose() + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + } - func setSelected(_ selected: Bool, animated: Bool = false) { + public func setSelected(_ selected: Bool, animated: Bool = false) { if self.isSelected != selected { self.isSelected = selected @@ -110,11 +146,11 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { } } - func setOverlayBackgroundColor(_ color: UIColor) { + public func setOverlayBackgroundColor(_ color: UIColor) { self.statusNode.backgroundNodeColor = color } - func setWallpaper(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) { + public func setWallpaper(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.imageNode.frame = CGRect(origin: CGPoint(), size: size) @@ -175,6 +211,11 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { self.backgroundNode.backgroundColor = UIColor(rgb: colors[0]) } } + + if isEmpty, let theme { + self.backgroundNode.image = nil + self.backgroundNode.backgroundColor = theme.list.mediaPlaceholderColor + } if let gradientNode = self.gradientNode { gradientNode.frame = CGRect(origin: CGPoint(), size: size) @@ -186,7 +227,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { let corners = ImageCorners(radius: cornerRadius) - if self.wallpaper != wallpaper { + if self.wallpaper != wallpaper && !isEmpty { self.wallpaper = wallpaper switch wallpaper { case .builtin: @@ -286,7 +327,7 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { } } else if let wallpaper = self.wallpaper { switch wallpaper { - case .builtin, .color, .gradient: + case .builtin, .color, .gradient, .emoticon: let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) apply() case let .image(representations, _): @@ -300,6 +341,51 @@ final class SettingsThemeWallpaperNode: ASDisplayNode { } self.setSelected(selected, animated: false) + + + self.emojiContainerNode.frame = self.backgroundNode.frame + + var emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 42.0) / 2.0), y: 98.0), size: CGSize(width: 42.0, height: 42.0)) + if isEmpty { + emojiFrame = emojiFrame.insetBy(dx: 3.0, dy: 3.0) + } + if let file = emojiFile, self.emojiFile?.id != emojiFile?.id { + self.emojiFile = file + + let imageApply = self.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) + imageApply() + self.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) + self.emojiImageNode.frame = emojiFrame + + let animatedStickerNode: AnimatedStickerNode + if let current = self.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = DefaultAnimatedStickerNodeImpl() + animatedStickerNode.started = { [weak self] in + self?.emojiImageNode.isHidden = true + } + self.animatedStickerNode = animatedStickerNode + self.emojiContainerNode.addSubnode(animatedStickerNode) + let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + } + animatedStickerNode.autoplay = true + animatedStickerNode.visibility = true + + self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + +// let thumbnailDimensions = PixelDimensions(width: 512, height: 512) +// self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) +// self.placeholderNode.frame = emojiFrame + } + + if let animatedStickerNode = self.animatedStickerNode { + animatedStickerNode.frame = emojiFrame + animatedStickerNode.updateLayout(size: emojiFrame.size) + } } @objc func buttonPressed() { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD new file mode 100644 index 00000000000..49b5ed0d226 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ThemeAccentColorScreen", + module_name = "ThemeAccentColorScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperBackgroundNode", + "//submodules/ComponentFlow", + "//submodules/SolidRoundedButtonNode", + "//submodules/AppBundle", + "//submodules/PremiumUI", + "//submodules/WallpaperResources", + "//submodules/HexColor", + "//submodules/MergeLists", + "//submodules/ShareController", + "//submodules/GalleryUI", + "//submodules/ChatListUI", + "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", + "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", + "//submodules/TelegramUI/Components/Settings/GenerateThemeName", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift similarity index 98% rename from submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift rename to submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift index 2624de8dd26..89eecec8947 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorController.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift @@ -10,16 +10,18 @@ import TelegramUIPreferences import AccountContext import PresentationDataUtils import MediaResources +import WallpaperGalleryScreen +import GenerateThemeName private let randomBackgroundColors: [Int32] = [0x007aff, 0x00c2ed, 0x29b327, 0xeb6ca4, 0xf08200, 0x9472ee, 0xd33213, 0xedb400, 0x6d839e] -extension TelegramThemeSettings { +public extension TelegramThemeSettings { convenience init(baseTheme: TelegramBaseTheme, accentColor: UIColor, outgoingAccentColor: UIColor?, messageColors: [UInt32], animateMessageColors: Bool, wallpaper: TelegramWallpaper?) { self.init(baseTheme: baseTheme, accentColor: accentColor.argb, outgoingAccentColor: outgoingAccentColor?.argb, messageColors: messageColors, animateMessageColors: animateMessageColors, wallpaper: wallpaper) } } -enum ThemeAccentColorControllerMode { +public enum ThemeAccentColorControllerMode { case colors(themeReference: PresentationThemeReference, create: Bool) case background(themeReference: PresentationThemeReference) case edit(settings: TelegramThemeSettings?, theme: PresentationTheme, wallpaper: TelegramWallpaper?, generalThemeReference: PresentationThemeReference?, defaultThemeReference: PresentationThemeReference?, create: Bool, completion: (PresentationTheme, TelegramThemeSettings?) -> Void) @@ -34,8 +36,8 @@ enum ThemeAccentColorControllerMode { } } -final class ThemeAccentColorController: ViewController { - enum ResultMode { +public final class ThemeAccentColorController: ViewController { + public enum ResultMode { case `default` case peer(EnginePeer) } @@ -52,7 +54,7 @@ final class ThemeAccentColorController: ViewController { } private let _ready = Promise() - override public var ready: Promise { + public override var ready: Promise { return self._ready } @@ -60,9 +62,9 @@ final class ThemeAccentColorController: ViewController { private var applyDisposable = MetaDisposable() - var completion: (() -> Void)? + public var completion: (() -> Void)? - init(context: AccountContext, mode: ThemeAccentColorControllerMode, resultMode: ResultMode = .default) { + public init(context: AccountContext, mode: ThemeAccentColorControllerMode, resultMode: ResultMode = .default) { self.context = context self.mode = mode self.resultMode = resultMode @@ -135,13 +137,13 @@ final class ThemeAccentColorController: ViewController { self.dismiss() } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.controllerNode.animateWallpaperAppeared() } - override func loadDisplayNode() { + public override func loadDisplayNode() { super.loadDisplayNode() let theme: PresentationTheme @@ -630,7 +632,7 @@ final class ThemeAccentColorController: ViewController { self.displayNodeDidLoad() } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + 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) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift similarity index 98% rename from submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift rename to submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 192e02a402b..a23fef5c55b 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -14,6 +14,7 @@ import PresentationDataUtils import WallpaperBackgroundNode import AnimationCache import MultiAnimationRenderer +import WallpaperGalleryScreen private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in @@ -308,7 +309,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .edit(_, _, _, _, _, true, _) = self.mode { doneButtonType = .proceed } else if case let .peer(peer) = resultMode { - doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium) + if peer.id.namespace == Namespaces.Peer.CloudUser { + doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium) + } else { + doneButtonType = .setChannel + } } else { doneButtonType = .set } @@ -775,7 +780,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .edit(_, _, _, _, _, true, _) = self.mode { doneButtonType = .proceed } else if case let .peer(peer) = self.resultMode { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + if peer.id.namespace == Namespaces.Peer.CloudUser { + doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + } else { + doneButtonType = .setChannel + } } else { doneButtonType = .set } @@ -856,10 +865,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) @@ -940,7 +950,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) @@ -1182,7 +1192,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .background = self.mode, toolbarBottomInset.isZero { toolbarBottomInset = 16.0 } - if case .peer = self.resultMode, !self.state.displayPatternPanel { + if case let .peer(peer) = self.resultMode, case .user = peer, !self.state.displayPatternPanel { toolbarBottomInset += 58.0 } let toolbarHeight = 49.0 + toolbarBottomInset diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/ThemeColorSegmentedTitleView.swift rename to submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift diff --git a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD new file mode 100644 index 00000000000..7e7c7672241 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ThemeCarouselItem", + module_name = "ThemeCarouselItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TelegramUIPreferences", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperResources", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/ShimmerEffect", + "//submodules/StickerResources", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/HexColor", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift similarity index 74% rename from submodules/SettingsUI/Sources/ThemeCarouselItem.swift rename to submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift index 7744e5a771d..95ebbd920bd 100644 --- a/submodules/SettingsUI/Sources/ThemeCarouselItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift @@ -18,12 +18,15 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import StickerResources +import ListItemComponentAdaptor +import HexColor private struct ThemeCarouselThemeEntry: Comparable, Identifiable { let index: Int let emojiFile: TelegramMediaFile? - let themeReference: PresentationThemeReference + let themeReference: PresentationThemeReference? let nightMode: Bool + let channelMode: Bool let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] var selected: Bool @@ -42,12 +45,15 @@ private struct ThemeCarouselThemeEntry: Comparable, Identifiable { if lhs.emojiFile?.fileId != rhs.emojiFile?.fileId { return false } - if lhs.themeReference.index != rhs.themeReference.index { + if lhs.themeReference?.index != rhs.themeReference?.index { return false } if lhs.nightMode != rhs.nightMode { return false } + if lhs.channelMode != rhs.channelMode { + return false + } if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { return false } @@ -73,31 +79,33 @@ private struct ThemeCarouselThemeEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(context: AccountContext, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { - return ThemeCarouselThemeIconItem(context: context, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action, contextAction: contextAction) + func item(context: AccountContext, action: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + return ThemeCarouselThemeIconItem(context: context, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, channelMode: self.channelMode, themeSpecificAccentColors: self.themeSpecificAccentColors, themeSpecificChatWallpapers: self.themeSpecificChatWallpapers, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action, contextAction: contextAction) } } -class ThemeCarouselThemeIconItem: ListViewItem { - let context: AccountContext - let emojiFile: TelegramMediaFile? - let themeReference: PresentationThemeReference - let nightMode: Bool - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let selected: Bool - let theme: PresentationTheme - let strings: PresentationStrings - let wallpaper: TelegramWallpaper? - let action: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? +public class ThemeCarouselThemeIconItem: ListViewItem { + public let context: AccountContext + public let emojiFile: TelegramMediaFile? + public let themeReference: PresentationThemeReference? + public let nightMode: Bool + public let channelMode: Bool + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let selected: Bool + public let theme: PresentationTheme + public let strings: PresentationStrings + public let wallpaper: TelegramWallpaper? + public let action: (PresentationThemeReference?) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - public init(context: AccountContext, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference, nightMode: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) { + public init(context: AccountContext, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, channelMode: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context self.emojiFile = emojiFile self.themeReference = themeReference self.nightMode = nightMode + self.channelMode = channelMode self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers self.selected = selected @@ -148,7 +156,6 @@ class ThemeCarouselThemeIconItem: ListViewItem { } } - private let textFont = Font.regular(12.0) private let selectedTextFont = Font.bold(12.0) @@ -329,6 +336,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { } func asyncLayout() -> (ThemeCarouselThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) let makeImageLayout = self.imageNode.asyncLayout() @@ -338,6 +346,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { var updatedThemeReference = false var updatedTheme = false var updatedNightMode = false + var updatedChannelMode = false var updatedWallpaper = false var updatedSelected = false @@ -347,6 +356,9 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { if currentItem?.nightMode != item.nightMode { updatedNightMode = true } + if currentItem?.channelMode != item.channelMode { + updatedChannelMode = true + } if currentItem?.wallpaper != item.wallpaper { updatedWallpaper = true } @@ -357,11 +369,16 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { updatedSelected = true } + let text = NSAttributedString(string: item.strings.Wallpaper_NoWallpaper, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) var string: String? - if let _ = item.themeReference.emoticon { + if item.themeReference == nil { + string = "❌" + self?.imageNode.backgroundColor = item.theme.list.mediaPlaceholderColor + } else if let _ = item.themeReference?.emoticon { } else { - string = "🎨" + string = item.channelMode ? "" : "🎨" } let emojiTitle = NSAttributedString(string: string ?? "", font: Font.regular(20.0), textColor: .black) @@ -372,17 +389,20 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { if let strongSelf = self { strongSelf.item = item - if updatedThemeReference || updatedWallpaper || updatedNightMode { - var themeReference = item.themeReference - if case .builtin = themeReference, item.nightMode { - themeReference = .builtin(.night) + if updatedThemeReference || updatedWallpaper || updatedNightMode || updatedChannelMode { + if var themeReference = item.themeReference { + if case .builtin = themeReference, item.nightMode { + themeReference = .builtin(.night) + } + + let color = item.themeSpecificAccentColors[themeReference.index] + let wallpaper = item.themeSpecificChatWallpapers[themeReference.index] + + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, channelMode: item.channelMode, emoticon: true)) + strongSelf.imageNode.backgroundColor = nil + } else { + } - - let color = item.themeSpecificAccentColors[themeReference.index] - let wallpaper = item.themeSpecificChatWallpapers[themeReference.index] - - strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: color, wallpaper: wallpaper ?? item.wallpaper, nightMode: item.nightMode, emoticon: true)) - strongSelf.imageNode.backgroundColor = nil } if updatedTheme || updatedSelected { @@ -413,6 +433,10 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 78.0), size: CGSize(width: 90.0, height: 30.0)) strongSelf.emojiNode.isHidden = string == nil + let _ = textApply() + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 24.0), size: textLayout.size) + strongSelf.textNode.isHidden = item.themeReference != nil + let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) if let file = item.emojiFile, currentItem?.emojiFile == nil { let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) @@ -451,7 +475,7 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { } let presentationData = item.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.activateAreaNode.accessibilityLabel = item.themeReference.emoticon.flatMap { presentationData.strings.Appearance_VoiceOver_Theme($0).string } + strongSelf.activateAreaNode.accessibilityLabel = item.themeReference?.emoticon.flatMap { presentationData.strings.Appearance_VoiceOver_Theme($0).string } if item.selected { strongSelf.activateAreaNode.accessibilityTraits = [.button, .selected] } else { @@ -507,31 +531,37 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { } } -class ThemeCarouselThemeItem: ListViewItem, ItemListItem { - var sectionId: ItemListSectionId - - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let themes: [PresentationThemeReference] - let animatedEmojiStickers: [String: [StickerPackItem]] - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let nightMode: Bool - let currentTheme: PresentationThemeReference - let updatedTheme: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - let tag: ItemListItemTag? - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { +public class ThemeCarouselThemeItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public var sectionId: ItemListSectionId + + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let themes: [PresentationThemeReference] + public let hasNoTheme: Bool + public let animatedEmojiStickers: [String: [StickerPackItem]] + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let nightMode: Bool + public let channelMode: Bool + public let selectedWallpaper: TelegramWallpaper? + public let currentTheme: PresentationThemeReference? + public let updatedTheme: (PresentationThemeReference?) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? + + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], hasNoTheme: Bool, animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, channelMode: Bool = false, selectedWallpaper: TelegramWallpaper? = nil, currentTheme: PresentationThemeReference?, updatedTheme: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { self.context = context self.theme = theme self.strings = strings self.themes = themes + self.hasNoTheme = hasNoTheme self.animatedEmojiStickers = animatedEmojiStickers self.themeSpecificAccentColors = themeSpecificAccentColors self.themeSpecificChatWallpapers = themeSpecificChatWallpapers self.nightMode = nightMode + self.channelMode = channelMode + self.selectedWallpaper = selectedWallpaper self.currentTheme = currentTheme self.updatedTheme = updatedTheme self.contextAction = contextAction @@ -539,7 +569,7 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + 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 = ThemeCarouselThemeItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -555,7 +585,7 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { } } - 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) { + 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? ThemeCarouselThemeItemNode { let makeLayout = nodeValue.asyncLayout() @@ -571,6 +601,45 @@ class ThemeCarouselThemeItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ThemeCarouselThemeItem, rhs: ThemeCarouselThemeItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.themes != rhs.themes { + return false + } + if lhs.animatedEmojiStickers != rhs.animatedEmojiStickers { + return false + } + if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { + return false + } + if lhs.themeSpecificChatWallpapers != rhs.themeSpecificChatWallpapers { + return false + } + if lhs.nightMode != rhs.nightMode { + return false + } + if lhs.channelMode != rhs.channelMode { + return false + } + if lhs.currentTheme != rhs.currentTheme { + return false + } + + return true + } } private struct ThemeCarouselThemeItemNodeTransition { @@ -582,7 +651,7 @@ private struct ThemeCarouselThemeItemNodeTransition { let updatePosition: Bool } -private func preparedTransition(context: AccountContext, action: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, from fromEntries: [ThemeCarouselThemeEntry], to toEntries: [ThemeCarouselThemeEntry], crossfade: Bool, updatePosition: Bool) -> ThemeCarouselThemeItemNodeTransition { +private func preparedTransition(context: AccountContext, action: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, from fromEntries: [ThemeCarouselThemeEntry], to toEntries: [ThemeCarouselThemeEntry], crossfade: Bool, updatePosition: Bool) -> ThemeCarouselThemeItemNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -596,7 +665,7 @@ private func ensureThemeVisible(listNode: ListView, themeReference: Presentation var resultNode: ThemeCarouselThemeItemIconNode? listNode.forEachItemNode { node in if resultNode == nil, let node = node as? ThemeCarouselThemeItemIconNode { - if node.item?.themeReference.index == themeReference.index { + if node.item?.themeReference?.index == themeReference.index { resultNode = node } } @@ -609,7 +678,7 @@ private func ensureThemeVisible(listNode: ListView, themeReference: Presentation } } -class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { +public class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -627,11 +696,11 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { private var tapping = false - var tag: ItemListItemTag? { + public var tag: ItemListItemTag? { return self.item?.tag } - init() { + public init() { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -654,7 +723,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.listNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -684,7 +753,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { var scrollToItem: ListViewScrollToItem? if !self.initialized || !self.tapping { if let index = transition.entries.firstIndex(where: { entry in - return entry.themeReference.index == item.currentTheme.index + return entry.themeReference?.index == item.currentTheme?.index }) { scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) self.initialized = true @@ -695,7 +764,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { }) } - func asyncLayout() -> (_ item: ThemeCarouselThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ThemeCarouselThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets @@ -728,20 +797,26 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { if strongSelf.maskNode.supernode == nil { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + strongSelf.backgroundNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -751,17 +826,20 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.bottomStripeNode.isHidden = true + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : 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)) } - - strongSelf.bottomStripeNode.isHidden = true 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.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) - 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)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 12.0 @@ -775,25 +853,35 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { var index: Int = 0 var hasCurrentTheme = false + if item.hasNoTheme { + let selected = item.currentTheme == nil + if selected { + hasCurrentTheme = true + } + entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: nil, nightMode: item.nightMode, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil)) + index += 1 + } for theme in item.themes { - let selected = item.currentTheme.index == theme.index + let selected = item.currentTheme?.index == theme.index if selected { hasCurrentTheme = true } let emojiFile = theme.emoticon.flatMap { item.animatedEmojiStickers[$0]?.first?.file } - entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: emojiFile, themeReference: theme, nightMode: item.nightMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil)) + entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: emojiFile, themeReference: theme, nightMode: item.nightMode, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: selected, theme: item.theme, strings: item.strings, wallpaper: nil)) index += 1 } if !hasCurrentTheme { - entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: nil)) + entries.insert(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: item.hasNoTheme ? item.selectedWallpaper : nil), at: item.hasNoTheme ? 1 : entries.count) } - let action: (PresentationThemeReference) -> Void = { [weak self] themeReference in + let action: (PresentationThemeReference?) -> Void = { [weak self] themeReference in if let strongSelf = self { strongSelf.tapping = true strongSelf.item?.updatedTheme(themeReference) - let _ = ensureThemeVisible(listNode: strongSelf.listNode, themeReference: themeReference, animated: true) + if let themeReference { + let _ = ensureThemeVisible(listNode: strongSelf.listNode, themeReference: themeReference, animated: true) + } Queue.mainQueue().after(0.4) { strongSelf.tapping = false } @@ -810,15 +898,15 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - func prepareCrossfadeTransition() { + public func prepareCrossfadeTransition() { guard self.snapshotView == nil else { return } @@ -836,7 +924,7 @@ class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { } } - func animateCrossfadeTransition() { + public func animateCrossfadeTransition() { guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { return } diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD new file mode 100644 index 00000000000..f0350af4f84 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD @@ -0,0 +1,33 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ThemeSettingsThemeItem", + module_name = "ThemeSettingsThemeItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/TelegramUIPreferences", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperResources", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/ContextUI", + "//submodules/HexColor", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift similarity index 85% rename from submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift rename to submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index 6fa79773ef7..64b0b365d35 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -14,6 +14,8 @@ import WallpaperResources import AccountContext import AppBundle import ContextUI +import ListItemComponentAdaptor +import HexColor private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int @@ -369,24 +371,24 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } -class ThemeSettingsThemeItem: ListViewItem, ItemListItem { - var sectionId: ItemListSectionId - - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let themes: [PresentationThemeReference] - let allThemes: [PresentationThemeReference] - let displayUnsupported: Bool - let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] - let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] - let themePreferredBaseTheme: [Int64: TelegramBaseTheme] - let currentTheme: PresentationThemeReference - let updatedTheme: (PresentationThemeReference) -> Void - let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? - let tag: ItemListItemTag? - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], allThemes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], themePreferredBaseTheme: [Int64: TelegramBaseTheme], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { +public class ThemeSettingsThemeItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + public var sectionId: ItemListSectionId + + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let themes: [PresentationThemeReference] + public let allThemes: [PresentationThemeReference] + public let displayUnsupported: Bool + public let themeSpecificAccentColors: [Int64: PresentationThemeAccentColor] + public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper] + public let themePreferredBaseTheme: [Int64: TelegramBaseTheme] + public let currentTheme: PresentationThemeReference + public let updatedTheme: (PresentationThemeReference) -> Void + public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)? + public let tag: ItemListItemTag? + + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], allThemes: [PresentationThemeReference], displayUnsupported: Bool, themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], themePreferredBaseTheme: [Int64: TelegramBaseTheme], currentTheme: PresentationThemeReference, updatedTheme: @escaping (PresentationThemeReference) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) { self.context = context self.theme = theme self.strings = strings @@ -403,7 +405,7 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + 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 = ThemeSettingsThemeItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -419,7 +421,7 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } - 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) { + 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? ThemeSettingsThemeItemNode { let makeLayout = nodeValue.asyncLayout() @@ -435,6 +437,42 @@ class ThemeSettingsThemeItem: ListViewItem, ItemListItem { } } } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: ThemeSettingsThemeItem, rhs: ThemeSettingsThemeItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.themes != rhs.themes { + return false + } + if lhs.displayUnsupported != rhs.displayUnsupported { + return false + } + if lhs.themeSpecificAccentColors != rhs.themeSpecificAccentColors { + return false + } + if lhs.themeSpecificChatWallpapers != rhs.themeSpecificChatWallpapers { + return false + } + if lhs.themePreferredBaseTheme != rhs.themePreferredBaseTheme { + return false + } + if lhs.currentTheme != rhs.currentTheme { + return false + } + + return true + } } private struct ThemeSettingsThemeItemNodeTransition { @@ -472,7 +510,7 @@ private func ensureThemeVisible(listNode: ListView, themeReference: Presentation } } -class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { +public class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { private let containerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -488,13 +526,13 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { private var item: ThemeSettingsThemeItem? private var layoutParams: ListViewItemLayoutParams? - var tag: ItemListItemTag? { + public var tag: ItemListItemTag? { return self.item?.tag } private var tapping = false - init() { + public init() { self.containerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() @@ -517,7 +555,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.listNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -558,7 +596,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { }) } - func asyncLayout() -> (_ item: ThemeSettingsThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + public func asyncLayout() -> (_ item: ThemeSettingsThemeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets @@ -591,20 +629,26 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { if strongSelf.maskNode.supernode == nil { strongSelf.containerNode.insertSubnode(strongSelf.maskNode, at: 3) } - - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + strongSelf.backgroundNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight @@ -614,15 +658,18 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : 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)) } 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.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) - 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)) var listInsets = UIEdgeInsets() listInsets.top += params.leftInset + 4.0 @@ -694,15 +741,15 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } - func prepareCrossfadeTransition() { + public func prepareCrossfadeTransition() { guard self.snapshotView == nil else { return } @@ -719,7 +766,7 @@ class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { } } - func animateCrossfadeTransition() { + public func animateCrossfadeTransition() { guard self.snapshotView?.layer.animationKeys()?.isEmpty ?? true else { return } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD new file mode 100644 index 00000000000..882bfd1377a --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD @@ -0,0 +1,39 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "WallpaperGalleryScreen", + module_name = "WallpaperGalleryScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperBackgroundNode", + "//submodules/ComponentFlow", + "//submodules/SolidRoundedButtonNode", + "//submodules/AppBundle", + "//submodules/PremiumUI", + "//submodules/WallpaperResources", + "//submodules/HexColor", + "//submodules/MergeLists", + "//submodules/ShareController", + "//submodules/GalleryUI", + "//submodules/CounterContollerTitleView", + "//submodules/LegacyMediaPickerUI", + "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/BlurredImageNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/BlurredImageNode.swift similarity index 91% rename from submodules/SettingsUI/Sources/BlurredImageNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/BlurredImageNode.swift index e9f0b77b393..722ed4ffee4 100644 --- a/submodules/SettingsUI/Sources/BlurredImageNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/BlurredImageNode.swift @@ -56,8 +56,8 @@ private class BlurLayer: CALayer { } } -class BlurView: UIView { - override class var layerClass : AnyClass { +public class BlurView: UIView { + public override class var layerClass : AnyClass { return BlurLayer.self } @@ -71,7 +71,7 @@ class BlurView: UIView { return Queue(name: nil, qos: .userInteractive) }() - open var blurRadius: CGFloat { + public var blurRadius: CGFloat { set { self.blurLayer.blurRadius = newValue } get { return self.blurLayer.blurRadius } } @@ -104,7 +104,7 @@ class BlurView: UIView { } } - override func display(_ layer: CALayer) { + public override func display(_ layer: CALayer) { let blurRadius = self.blurLayer.presentationRadius if let image = self.image { self.draw(image, blurRadius: blurRadius) @@ -112,19 +112,19 @@ class BlurView: UIView { } } -final class BlurredImageNode: ASDisplayNode { - var image: UIImage? { +public final class BlurredImageNode: ASDisplayNode { + public var image: UIImage? { didSet { self.blurView.image = self.image self.blurView.layer.setNeedsDisplay() } } - var blurView: BlurView { + public var blurView: BlurView { return (self.view as? BlurView)! } - override init() { + public override init() { super.init() self.setViewBlock({ diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift similarity index 94% rename from submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift index bc19dd7e5d7..b4f8d027400 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift @@ -313,15 +313,26 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } } -struct WallpaperColorPanelNodeState: Equatable { - var selection: Int? - var colors: [HSBColor] - var maximumNumberOfColors: Int - var rotateAvailable: Bool - var rotation: Int32 - var preview: Bool - var simpleGradientGeneration: Bool - var suggestedNewColor: HSBColor? +public struct WallpaperColorPanelNodeState: Equatable { + public var selection: Int? + public var colors: [HSBColor] + public var maximumNumberOfColors: Int + public var rotateAvailable: Bool + public var rotation: Int32 + 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 + self.maximumNumberOfColors = maximumNumberOfColors + self.rotateAvailable = rotateAvailable + self.rotation = rotation + self.preview = preview + self.simpleGradientGeneration = simpleGradientGeneration + self.suggestedNewColor = suggestedNewColor + } } private final class ColorSampleItemNode: ASImageNode { @@ -377,7 +388,7 @@ private final class ColorSampleItemNode: ASImageNode { } } -final class WallpaperColorPanelNode: ASDisplayNode { +public final class WallpaperColorPanelNode: ASDisplayNode { private var theme: PresentationTheme private var state: WallpaperColorPanelNodeState @@ -394,16 +405,16 @@ final class WallpaperColorPanelNode: ASDisplayNode { private var sampleItemNodes: [ColorSampleItemNode] = [] private let multiColorFieldNode: ColorInputFieldNode - var colorsChanged: (([HSBColor], Int, Bool) -> Void)? - var colorSelected: (() -> Void)? - var rotate: (() -> Void)? + public var colorsChanged: (([HSBColor], Int, Bool) -> Void)? + public var colorSelected: (() -> Void)? + public var rotate: (() -> Void)? - var colorAdded: (() -> Void)? - var colorRemoved: (() -> Void)? + public var colorAdded: (() -> Void)? + public var colorRemoved: (() -> Void)? private var validLayout: (CGSize, CGFloat)? - init(theme: PresentationTheme, strings: PresentationStrings) { + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) @@ -512,7 +523,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { } } - func updateTheme(_ theme: PresentationTheme) { + 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 @@ -520,7 +531,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { self.multiColorFieldNode.updateTheme(theme) } - func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, animated: Bool = true) { + 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 @@ -560,7 +571,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { } } - func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, bottomInset) let condensedLayout = size.width < 375.0 @@ -724,7 +735,7 @@ final class WallpaperColorPanelNode: ASDisplayNode { }) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = super.hitTest(point, with: event) { return result } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift similarity index 97% rename from submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift index 50e51a2aeff..ec5198f61f0 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperColorPickerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift @@ -266,50 +266,50 @@ private final class WallpaperColorBrightnessNode: ASDisplayNode { } } -struct HSBColor: Equatable { - static func == (lhs: HSBColor, rhs: HSBColor) -> Bool { +public struct HSBColor: Equatable { + public static func == (lhs: HSBColor, rhs: HSBColor) -> Bool { return lhs.values.h == rhs.values.h && lhs.values.s == rhs.values.s && lhs.values.b == rhs.values.b } - let values: (h: CGFloat, s: CGFloat, b: CGFloat) - let backingColor: UIColor + public let values: (h: CGFloat, s: CGFloat, b: CGFloat) + public let backingColor: UIColor - var hue: CGFloat { + public var hue: CGFloat { return self.values.h } - var saturation: CGFloat { + public var saturation: CGFloat { return self.values.s } - var brightness: CGFloat { + public var brightness: CGFloat { return self.values.b } - var rgb: UInt32 { + public var rgb: UInt32 { return self.color.argb } - init(values: (h: CGFloat, s: CGFloat, b: CGFloat)) { + public init(values: (h: CGFloat, s: CGFloat, b: CGFloat)) { self.values = values self.backingColor = UIColor(hue: values.h, saturation: values.s, brightness: values.b, alpha: 1.0) } - init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat) { + public init(hue: CGFloat, saturation: CGFloat, brightness: CGFloat) { self.values = (h: hue, s: saturation, b: brightness) self.backingColor = UIColor(hue: self.values.h, saturation: self.values.s, brightness: self.values.b, alpha: 1.0) } - init(color: UIColor) { + public init(color: UIColor) { self.values = color.hsb self.backingColor = color } - init(rgb: UInt32) { + public init(rgb: UInt32) { self.init(color: UIColor(rgb: rgb)) } - var color: UIColor { + public var color: UIColor { return self.backingColor } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperCropNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/WallpaperCropNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift similarity index 99% rename from submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 18c55a2f023..c306b30e67d 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -234,6 +234,8 @@ public class WallpaperGalleryController: ViewController { private var savedPatternWallpaper: TelegramWallpaper? private var savedPatternIntensity: Int32? + public var requiredLevel: Int? + public init(context: AccountContext, source: WallpaperListSource, mode: Mode = .default) { self.context = context self.source = source @@ -401,7 +403,7 @@ public class WallpaperGalleryController: ViewController { self.colorsPanelNode?.updateTheme(self.presentationData.theme) } - func dismiss(forceAway: Bool) { + public func dismiss(forceAway: Bool) { // let completion: () -> Void = { [weak self] in // self?.presentingViewController?.dismiss(animated: false, completion: nil) // } @@ -498,10 +500,15 @@ public class WallpaperGalleryController: ViewController { break } if case let .peer(peer, _) = self.mode { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + 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 @@ -969,7 +976,7 @@ public class WallpaperGalleryController: ViewController { self.overlayNode?.frame = self.galleryNode.bounds var toolbarHeight: CGFloat = 66.0 - if case .peer = self.mode { + 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))) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift similarity index 94% rename from submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index 01fb70c683e..84ecd5cac6d 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -716,7 +716,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.initialWallpaper = wallpaper switch wallpaper { - case .builtin: + case .builtin, .emoticon: displaySize = CGSize(width: 1308.0, height: 2688.0).fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor contentSize = displaySize signal = settingsBuiltinWallpaperImage(account: self.context.account) @@ -1330,7 +1330,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let buttonSpacing: CGFloat = 18.0 var toolbarHeight: CGFloat = 66.0 - if let mode = self.mode, case .peer = mode { + if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer { toolbarHeight += 58.0 } @@ -1370,7 +1370,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { 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)) let centerOffset: CGFloat = 32.0 - + + var isPattern = false if let entry = self.entry { switch entry { case .asset: @@ -1385,7 +1386,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { motionFrame = rightButtonFrame case let .wallpaper(wallpaper, _): switch wallpaper { - case .builtin: + case .builtin, .emoticon: motionAlpha = 1.0 motionFrame = centerButtonFrame case .color: @@ -1417,6 +1418,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { colorsAlpha = 1.0 case let .file(file): if file.isPattern { + isPattern = true + motionAlpha = 0.0 patternAlpha = 1.0 @@ -1451,6 +1454,18 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } } + if let mode = self.mode, case let .peer(peer, _) = mode, case .channel = peer { + motionAlpha = 0.0 + if isPattern { + patternAlpha = 0.0 + colorsAlpha = 0.0 + blurAlpha = 0.0 + playAlpha = 0.0 + self.shareButtonNode.isHidden = true + } + blurFrame = centerButtonFrame + } + transition.updateFrame(node: self.patternButtonNode, frame: patternFrame) transition.updateAlpha(node: self.patternButtonNode, alpha: patternAlpha * alpha) @@ -1482,18 +1497,38 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.nativeNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) var bottomInset: CGFloat = 132.0 - if let mode = self.mode, case .peer = mode { - bottomInset += 58.0 + if let mode = self.mode, case let .peer(peer, _) = mode { + if case .user = peer { + bottomInset += 58.0 + } else if case .channel = peer, let entry = self.entry, case let .wallpaper(wallpaper, _) = entry, case let .file(file) = wallpaper, file.isPattern { + bottomInset -= 42.0 + } } var items: [ListViewItem] = [] - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + var peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) let otherPeerId = self.context.account.peerId var peers = SimpleDictionary() - let messages = SimpleDictionary() - peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) - peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + var messages = SimpleDictionary() + + let replyAuthor = self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName + + var messageAuthor: Peer = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_PreviewReplyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + let otherAuthor = TelegramUser(id: otherPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .blue, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + peers[otherPeerId] = otherAuthor + var messageAttributes: [MessageAttribute] = [] + if let mode = self.mode, case let .peer(peer, _) = mode, case .channel = peer { + peerId = peer.id + messageAuthor = peer._asPeer() + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageAuthor, text: self.presentationData.strings.WallpaperPreview_ChannelReplyText, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + messageAttributes = [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)] + } + + peers[peerId] = messageAuthor + var topMessageText = "" var bottomMessageText = "" var serviceMessageText: String? @@ -1566,10 +1601,16 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } if let mode = self.mode, case let .peer(peer, existing) = mode { - topMessageText = presentationData.strings.WallpaperPreview_ChatTopText - bottomMessageText = presentationData.strings.WallpaperPreview_ChatBottomText - if !existing { - serviceMessageText = presentationData.strings.WallpaperPreview_NotAppliedInfo(peer.compactDisplayTitle).string + if case .channel = peer { + topMessageText = presentationData.strings.WallpaperPreview_ChannelTopText + bottomMessageText = "" + serviceMessageText = presentationData.strings.WallpaperPreview_ChannelHeader + } else { + topMessageText = presentationData.strings.WallpaperPreview_ChatTopText + bottomMessageText = presentationData.strings.WallpaperPreview_ChatBottomText + if !existing { + serviceMessageText = presentationData.strings.WallpaperPreview_NotAppliedInfo(peer.compactDisplayTitle).string + } } } @@ -1579,17 +1620,20 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let theme = self.presentationData.theme - let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + if !bottomMessageText.isEmpty { + let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) + } + - let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: messageAttributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) if let serviceMessageText { let attributedText = convertMarkdownToAttributes(NSAttributedString(string: serviceMessageText)) let entities = generateChatInputTextEntities(attributedText) - let message3 = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: attributedText.string, entities: entities))], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message3 = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: attributedText.string, entities: entities, additionalAttributes: nil))], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil, accountPeer: nil, isCentered: false)) } @@ -1637,7 +1681,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if let _ = serviceMessageText, let messageNodes = self.messageNodes, let node = messageNodes.last { if let backgroundNode = node.subnodes?.first?.subnodes?.first?.subnodes?.first?.subnodes?.first, let backdropNode = node.subnodes?.first?.subnodes?.first?.subnodes?.first?.subnodes?.last?.subnodes?.last?.subnodes?.first { - backdropNode.isHidden = true + if !(backdropNode is TextNode) { + backdropNode.isHidden = true + } let serviceBackgroundFrame = backgroundNode.view.convert(backgroundNode.bounds, to: self.view).offsetBy(dx: 0.0, dy: -1.0).insetBy(dx: 0.0, dy: -1.0) transition.updateFrame(node: self.serviceBackgroundNode, frame: serviceBackgroundFrame) self.serviceBackgroundNode.update(size: serviceBackgroundFrame.size, cornerRadius: serviceBackgroundFrame.height / 2.0, transition: transition) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift similarity index 79% rename from submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift index 876f1325bb3..0ce64b7b9c5 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift @@ -4,21 +4,24 @@ import AsyncDisplayKit import Display import TelegramPresentationData import ManagedAnimationNode +import ComponentFlow +import PremiumLockButtonSubtitleComponent -enum WallpaperGalleryToolbarCancelButtonType { +public enum WallpaperGalleryToolbarCancelButtonType { case cancel case discard } -enum WallpaperGalleryToolbarDoneButtonType { +public enum WallpaperGalleryToolbarDoneButtonType { case set case setPeer(String, Bool) + case setChannel case proceed case apply case none } -protocol WallpaperGalleryToolbar: ASDisplayNode { +public protocol WallpaperGalleryToolbar: ASDisplayNode { var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { get set } var doneButtonType: WallpaperGalleryToolbarDoneButtonType { get set } @@ -30,11 +33,15 @@ protocol WallpaperGalleryToolbar: ASDisplayNode { func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) } -final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { +public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { class ButtonNode: ASDisplayNode { + private let strings: PresentationStrings + private let doneButton = HighlightTrackingButtonNode() private var doneButtonBackgroundNode: ASDisplayNode private let doneButtonTitleNode: ImmediateTextNode + private var doneButtonSubtitle: ComponentView? + private let doneButtonSolidBackgroundNode: ASDisplayNode private let doneButtonSolidTitleNode: ImmediateTextNode @@ -48,7 +55,11 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar } } - override init() { + var requiredLevel: Int? + + init(strings: PresentationStrings) { + self.strings = strings + self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode() self.doneButtonBackgroundNode.cornerRadius = 14.0 @@ -101,6 +112,9 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar 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 { @@ -113,6 +127,9 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar 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) } } } @@ -167,7 +184,45 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar 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) - self.doneButtonTitleNode.frame = 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) + + 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 { + subtitle = current + } else { + subtitle = ComponentView() + self.doneButtonSubtitle = subtitle + } + + let subtitleSize = subtitle.update( + transition: .immediate, + component: AnyComponent( + PremiumLockButtonSubtitleComponent( + count: requiredLevel, + color: UIColor(rgb: 0xffffff, alpha: 0.7), + strings: self.strings + ) + ), + 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 @@ -204,36 +259,45 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar private var theme: PresentationTheme private let strings: PresentationStrings - var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - var doneButtonType: WallpaperGalleryToolbarDoneButtonType { + public var doneButtonType: WallpaperGalleryToolbarDoneButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - var dark: Bool = false { + public var dark: Bool = false { didSet { self.applyButton.dark = self.dark self.applyForBothButton.dark = self.dark } } - private let applyButton = ButtonNode() - private let applyForBothButton = ButtonNode() + private let applyButton: ButtonNode + private let applyForBothButton: ButtonNode - var cancel: (() -> Void)? - var done: ((Bool) -> Void)? + public var cancel: (() -> Void)? + public var done: ((Bool) -> Void)? - init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { + 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) @@ -255,13 +319,13 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar } } - func setDoneEnabled(_ enabled: Bool) { + public func setDoneEnabled(_ enabled: Bool) { self.applyButton.setEnabled(enabled) self.applyForBothButton.setEnabled(enabled) } private var isSolid = false - func setDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { + public func setDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { guard self.isSolid != isSolid else { return } @@ -271,7 +335,7 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar self.applyForBothButton.setIsSolid(isSolid, transition: transition) } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme let applyTitle: String @@ -284,6 +348,8 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar applyTitle = strings.Wallpaper_ApplyForMe applyForBothTitle = strings.Wallpaper_ApplyForBoth(name).string applyForBothLocked = !isPremium + case .setChannel: + applyTitle = strings.Wallpaper_ApplyForChannel case .proceed: applyTitle = strings.Theme_Colors_Proceed case .apply: @@ -300,7 +366,7 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar self.applyForBothButton.isLocked = applyForBothLocked } - func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let inset: CGFloat = 16.0 let buttonHeight: CGFloat = 50.0 @@ -326,16 +392,16 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar } } -final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { +public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { private var theme: PresentationTheme private let strings: PresentationStrings - var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - var doneButtonType: WallpaperGalleryToolbarDoneButtonType { + public var doneButtonType: WallpaperGalleryToolbarDoneButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } @@ -349,10 +415,10 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb private let separatorNode = ASDisplayNode() private let topSeparatorNode = ASDisplayNode() - var cancel: (() -> Void)? - var done: ((Bool) -> Void)? + public var cancel: (() -> Void)? + public var done: ((Bool) -> Void)? - init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { + public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme self.strings = strings self.cancelButtonType = cancelButtonType @@ -401,12 +467,12 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) } - func setDoneEnabled(_ enabled: Bool) { + public func setDoneEnabled(_ enabled: Bool) { self.doneButton.alpha = enabled ? 1.0 : 0.4 self.doneButton.isUserInteractionEnabled = enabled } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + 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 @@ -423,7 +489,7 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb } let doneTitle: String switch self.doneButtonType { - case .set, .setPeer: + case .set, .setPeer, .setChannel: doneTitle = strings.Wallpaper_Set case .proceed: doneTitle = strings.Theme_Colors_Proceed @@ -437,7 +503,7 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb self.doneButton.setTitle(doneTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) } - func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + 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)) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift similarity index 97% rename from submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift index 281dc083397..4129277be36 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperOptionButtonNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift @@ -6,7 +6,7 @@ import SwiftSignalKit import CheckNode import AnimationUI -enum WallpaperOptionButtonValue { +public enum WallpaperOptionButtonValue { case check(Bool) case color(Bool, UIColor) case colors(Bool, [UIColor]) @@ -270,7 +270,7 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { } -final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { +public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { let backgroundNode: WallpaperOptionBackgroundNode private let checkNode: CheckNode @@ -281,7 +281,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { private var textSize: CGSize? private var _value: WallpaperOptionButtonValue - override var isSelected: Bool { + public override var isSelected: Bool { get { switch self._value { case let .check(selected), let .color(selected, _), let .colors(selected, _): @@ -301,13 +301,13 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { } } - var title: String { + public var title: String { didSet { self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white) } } - init(title: String, value: WallpaperOptionButtonValue) { + public init(title: String, value: WallpaperOptionButtonValue) { self._value = value self.title = title @@ -368,12 +368,12 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { } } - var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { + public var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { didSet { } } - var color: UIColor? { + public var color: UIColor? { get { switch self._value { case let .color(_, color): @@ -395,7 +395,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { } } - var colors: [UIColor]? { + public var colors: [UIColor]? { get { switch self._value { case let .colors(_, colors): @@ -429,7 +429,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { } } - func setSelected(_ selected: Bool, animated: Bool = false) { + public func setSelected(_ selected: Bool, animated: Bool = false) { switch self._value { case .check: self._value = .check(selected) @@ -441,7 +441,7 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { self.checkNode.setSelected(selected, animated: animated) } - func setEnabled(_ enabled: Bool) { + public func setEnabled(_ enabled: Bool) { let alpha: CGFloat = enabled ? 1.0 : 0.4 self.checkNode.alpha = alpha self.colorNode.alpha = alpha @@ -449,13 +449,13 @@ final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { self.isUserInteractionEnabled = enabled } - override func measure(_ constrainedSize: CGSize) -> CGSize { + public override func measure(_ constrainedSize: CGSize) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) self.textSize = size return CGSize(width: ceil(size.width) + 48.0, height: 30.0) } - override func layout() { + public override func layout() { super.layout() self.backgroundNode.frame = self.bounds diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift similarity index 96% rename from submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift index c4be43adef7..508c0bb11b3 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperPatternPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift @@ -9,6 +9,7 @@ import LegacyComponents import AccountContext import MergeLists import Postbox +import SettingsThemeWallpaperNode private let itemSize = CGSize(width: 88.0, height: 88.0) private let inset: CGFloat = 12.0 @@ -172,24 +173,24 @@ private final class WallpaperPatternItemNode : ListViewItemNode { } } -final class WallpaperPatternPanelNode: ASDisplayNode { +public final class WallpaperPatternPanelNode: ASDisplayNode { private let context: AccountContext private var theme: PresentationTheme private let backgroundNode: NavigationBackgroundNode private let topSeparatorNode: ASDisplayNode - let scrollNode: ASScrollNode + public let scrollNode: ASScrollNode private let titleNode: ImmediateTextNode private let labelNode: ImmediateTextNode private var sliderView: TGPhotoEditorSliderView? private var disposable: Disposable? - var wallpapers: [TelegramWallpaper] = [] + public var wallpapers: [TelegramWallpaper] = [] private var currentWallpaper: TelegramWallpaper? - var serviceBackgroundColor: UIColor = UIColor(rgb: 0x748698) { + public var serviceBackgroundColor: UIColor = UIColor(rgb: 0x748698) { didSet { guard let nodes = self.scrollNode.subnodes else { return @@ -200,7 +201,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - var backgroundColors: ([HSBColor], Int32?, Int32?)? = nil { + public var backgroundColors: ([HSBColor], Int32?, Int32?)? = nil { didSet { var updated = false if oldValue?.0 != self.backgroundColors?.0 || oldValue?.1 != self.backgroundColors?.1 { @@ -223,11 +224,11 @@ final class WallpaperPatternPanelNode: ASDisplayNode { private var validLayout: (CGSize, CGFloat)? - var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)? + public var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)? private let allowDark: Bool - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { self.context = context self.theme = theme self.allowDark = theme.overallDarkAppearance @@ -287,7 +288,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.disposable?.dispose() } - override func didLoad() { + public override func didLoad() { super.didLoad() self.scrollNode.view.showsHorizontalScrollIndicator = false @@ -320,7 +321,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.sliderView = sliderView } - func updateWallpapers() { + public func updateWallpapers() { guard let subnodes = self.scrollNode.subnodes else { return } @@ -386,7 +387,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { self.layoutItemNodes(transition: .immediate) } - func updateTheme(_ theme: PresentationTheme) { + public func updateTheme(_ theme: PresentationTheme) { self.theme = theme self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) @@ -416,7 +417,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) { + public func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) { let wallpaper: TelegramWallpaper? if self.wallpapers.isEmpty { @@ -482,7 +483,7 @@ final class WallpaperPatternPanelNode: ASDisplayNode { } } - func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, bottomInset) let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height + bottomInset) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/BUILD b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/BUILD new file mode 100644 index 00000000000..e88a5216fda --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/BUILD @@ -0,0 +1,44 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "WallpaperGridScreen", + module_name = "WallpaperGridScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/PresentationDataUtils", + "//submodules/WallpaperBackgroundNode", + "//submodules/ComponentFlow", + "//submodules/SolidRoundedButtonNode", + "//submodules/AppBundle", + "//submodules/PremiumUI", + "//submodules/WallpaperResources", + "//submodules/HexColor", + "//submodules/MergeLists", + "//submodules/ShareController", + "//submodules/GalleryUI", + "//submodules/GridMessageSelectionNode", + "//submodules/SearchUI", + "//submodules/MediaPickerUI", + "//submodules/ItemListPeerActionItem", + "//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen", + "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", + "//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen", + "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + "//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift similarity index 99% rename from submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index acce3f33a9c..d7a193baded 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -9,6 +9,8 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import AttachmentUI +import WallpaperGalleryScreen +import ThemeAccentColorScreen private func availableGradients(dark: Bool) -> [[UInt32]] { if dark { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift similarity index 98% rename from submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift index aae0ab87c43..242cffb640e 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import AsyncDisplayKit import AccountContext import GridMessageSelectionNode +import SettingsThemeWallpaperNode final class ThemeColorsGridControllerItem: GridItem { let context: AccountContext diff --git a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift similarity index 99% rename from submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift index 1b6ac7c1218..e61e5e7906d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeColorsGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift @@ -9,6 +9,7 @@ import MergeLists import ItemListUI import PresentationDataUtils import AccountContext +import WallpaperGalleryScreen final class ThemeColorsGridControllerInteraction { let openWallpaper: (TelegramWallpaper) -> Void @@ -377,7 +378,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { let makeColorLayout = self.customColorItemNode.asyncLayout() let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: listInsets.left, rightInset: listInsets.right, availableHeight: layout.size.height) let (colorLayout, colorApply) = makeColorLayout(self.customColorItem, params, ItemListNeighbors(top: .none, bottom: .none)) - colorApply() + 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))) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift similarity index 72% rename from submodules/SettingsUI/Sources/Themes/ThemeGridController.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index 227edafcc92..71edb41aa56 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -15,8 +15,19 @@ import SearchUI import HexColor import PresentationDataUtils import MediaPickerUI +import WallpaperGalleryScreen + +public enum WallpaperSelectionResult { + case remove + case emoticon(String) + case custom(wallpaperEntry: WallpaperGalleryEntry, options: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?) +} public final class ThemeGridController: ViewController { + public enum Mode { + case generic + case peer(EnginePeer, [TelegramTheme], TelegramWallpaper?, Int?, Int?) + } private var controllerNode: ThemeGridControllerNode { return self.displayNode as! ThemeGridControllerNode } @@ -27,6 +38,7 @@ public final class ThemeGridController: ViewController { } private let context: AccountContext + private let mode: Mode private var presentationData: PresentationData private let presentationDataPromise = Promise() @@ -45,14 +57,24 @@ public final class ThemeGridController: ViewController { private var previousContentOffset: GridNodeVisibleContentOffset? - public init(context: AccountContext) { + public var completion: (WallpaperSelectionResult) -> Void = { _ in } + + public init(context: AccountContext, mode: Mode = .generic) { self.context = context + self.mode = mode + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise.set(.single(self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) - self.title = self.presentationData.strings.Wallpaper_Title + switch mode { + case .generic: + self.title = self.presentationData.strings.Wallpaper_Title + case .peer: + self.title = self.presentationData.strings.Wallpaper_ChannelTitle + } + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style @@ -80,10 +102,12 @@ public final class ThemeGridController: ViewController { } }) - self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search, activate: { [weak self] in - self?.activateSearch() - }) - self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + if case .generic = mode { + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } } required public init(coder aDecoder: NSCoder) { @@ -98,12 +122,14 @@ public final class ThemeGridController: ViewController { self.title = self.presentationData.strings.Wallpaper_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - 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)) + if case .generic = self.mode { + if let isEmpty = self.isEmpty, isEmpty { } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + if self.editingMode { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_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)) + } } } @@ -117,25 +143,64 @@ public final class ThemeGridController: ViewController { } public override func loadDisplayNode() { - self.displayNode = ThemeGridControllerNode(context: self.context, presentationData: self.presentationData, presentPreviewController: { [weak self] source in + var mode: WallpaperGalleryController.Mode = .default + var requiredLevel: Int? + var requiredCustomLevel: Int? + if case let .peer(peer, _, _, requiredLevelValue, requiredCustomLevelValue) = self.mode { + mode = .peer(peer, false) + requiredLevel = requiredLevelValue + requiredCustomLevel = requiredCustomLevelValue + } + + self.displayNode = ThemeGridControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, presentPreviewController: { [weak self] source in if let strongSelf = self { - let controller = WallpaperGalleryController(context: strongSelf.context, source: source) + let dismissControllers = { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + var controllers = navigationController.viewControllers.filter({ controller in + if controller is ThemeGridController { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: false) + + controllers = navigationController.viewControllers.filter({ controller in + if controller is WallpaperGalleryController { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: true) + } + } + + let controller = WallpaperGalleryController(context: strongSelf.context, source: source, mode: mode) + controller.requiredLevel = requiredLevel controller.apply = { [weak self, weak controller] wallpaper, options, editedImage, cropRect, brightness, _ in if let strongSelf = self { - uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in - if let strongSelf = self { - strongSelf.deactivateSearch(animated: false) - strongSelf.controllerNode.scrollToTop(animated: false) + if case .peer = mode { + var emoticon = "" + if case let .wallpaper(wallpaper, _) = wallpaper { + emoticon = wallpaper.settings?.emoticon ?? "" } - if let controller = controller { - switch wallpaper { - case .asset, .contextResult: - controller.dismiss(animated: true) - default: - break + strongSelf.completion(.emoticon(emoticon)) + dismissControllers() + } else { + uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in + if let strongSelf = self { + strongSelf.deactivateSearch(animated: false) + strongSelf.controllerNode.scrollToTop(animated: false) } - } - }) + if let controller = controller { + switch wallpaper { + case .asset, .contextResult: + controller.dismiss(animated: true) + default: + break + } + } + }) + } } } self?.push(controller) @@ -144,13 +209,32 @@ public final class ThemeGridController: ViewController { if let strongSelf = self { let dismissControllers = { [weak self] in if let self, let navigationController = self.navigationController as? NavigationController { - let controllers = navigationController.viewControllers.filter({ controller in - if controller is WallpaperGalleryController || controller is MediaPickerScreen { - return false - } - return true - }) - navigationController.setViewControllers(controllers, animated: true) + if case .peer = mode { + var controllers = navigationController.viewControllers.filter({ controller in + if controller is ThemeGridController || controller is MediaPickerScreen { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: false) + + controllers = navigationController.viewControllers.filter({ controller in + if controller is WallpaperGalleryController { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: true) + } else { + let controllers = navigationController.viewControllers.filter({ controller in + if controller is WallpaperGalleryController || controller is MediaPickerScreen { + return false + } + + return true + }) + navigationController.setViewControllers(controllers, animated: true) + } } } @@ -159,12 +243,20 @@ public final class ThemeGridController: ViewController { guard let strongSelf = self, let asset = asset as? PHAsset else { return } - let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset)) + let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: mode) + controller.requiredLevel = requiredCustomLevel controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, _ in if let strongSelf = self { - uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { - dismissControllers() - }) + if case .peer = mode { + strongSelf.completion(.custom(wallpaperEntry: wallpaper, options: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness)) + Queue.mainQueue().after(0.15) { + dismissControllers() + } + } else { + uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { + dismissControllers() + }) + } } } strongSelf.push(controller) @@ -181,13 +273,15 @@ public final class ThemeGridController: ViewController { if empty != strongSelf.isEmpty { strongSelf.isEmpty = empty - if empty { - strongSelf.navigationItem.setRightBarButton(nil, animated: true) - } else { - if strongSelf.editingMode { - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + if case .generic = strongSelf.mode { + if empty { + strongSelf.navigationItem.setRightBarButton(nil, animated: true) } else { - strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + if strongSelf.editingMode { + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) + } else { + strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) + } } } } @@ -307,7 +401,12 @@ public final class ThemeGridController: ViewController { self.controllerNode.requestDeactivateSearch = { [weak self] in self?.deactivateSearch(animated: true) } - + self.controllerNode.requestWallpaperRemoval = { [weak self] in + if let self { + self.completion(.remove) + self.dismiss() + } + } self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { if let searchContentNode = strongSelf.searchContentNode { diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift new file mode 100644 index 00000000000..c65730aeb25 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift @@ -0,0 +1,229 @@ +import Foundation +import UIKit +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox +import AccountContext +import GridMessageSelectionNode +import SettingsThemeWallpaperNode +import TelegramPresentationData + +private var cachedBorderImages: [String: UIImage] = [:] +private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { + let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)" + if let image = cachedBorderImages[key] { + return image + } else { + let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth: CGFloat + if selected { + lineWidth = 2.0 + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor) + + context.strokeEllipse(in: bounds.insetBy(dx: 2.0 + lineWidth / 2.0, dy: 2.0 + lineWidth / 2.0)) + + var accentColor = theme.list.itemAccentColor + if accentColor.rgb == 0xffffff { + accentColor = UIColor(rgb: 0x999999) + } + context.setStrokeColor(accentColor.cgColor) + } else { + context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) + lineWidth = 1.0 + } + + if bordered || selected { + context.setLineWidth(lineWidth) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + } + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + cachedBorderImages[key] = image + return image + } +} + +final class ThemeGridControllerItem: GridItem { + let context: AccountContext + let theme: PresentationTheme? + let wallpaper: TelegramWallpaper + let wallpaperId: ThemeGridControllerEntry.StableId + let isEmpty: Bool + let emojiFile: TelegramMediaFile? + let channelMode: Bool + let index: Int + let editable: Bool + let selected: Bool + let interaction: ThemeGridControllerInteraction + + let section: GridSection? = nil + + init(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, channelMode: Bool = false, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) { + self.context = context + self.theme = theme + self.wallpaper = wallpaper + self.wallpaperId = wallpaperId + self.isEmpty = isEmpty + self.emojiFile = emojiFile + self.channelMode = channelMode + self.index = index + self.editable = editable + self.selected = selected + self.interaction = interaction + } + + func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + let node = ThemeGridControllerItemNode() + node.setup(item: self, synchronousLoad: synchronousLoad) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? ThemeGridControllerItemNode else { + assertionFailure() + return + } + node.setup(item: self, synchronousLoad: false) + } +} + +final class ThemeGridControllerItemNode: GridItemNode { + private let wallpaperNode: SettingsThemeWallpaperNode + private var selectionNode: GridMessageSelectionNode? + private var selectionBorderNode: ASImageNode? + + private var textNode: ImmediateTextNode? + + private var item: ThemeGridControllerItem? + + override init() { + self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.wallpaperNode) + } + + override func didLoad() { + super.didLoad() + + self.view.layer.cornerRadius = 10.0 + + self.view.isExclusiveTouch = true + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) { + self.item = item + self.updateSelectionState(animated: false) + + if item.channelMode, item.selected, let theme = item.theme { + let selectionBorderNode: ASImageNode + if let current = self.selectionBorderNode { + selectionBorderNode = current + } else { + selectionBorderNode = ASImageNode() + selectionBorderNode.displaysAsynchronously = false + self.selectionBorderNode = selectionBorderNode + + self.addSubnode(selectionBorderNode) + } + + selectionBorderNode.image = generateBorderImage(theme: theme, bordered: true, selected: true) + } else { + self.selectionBorderNode?.removeFromSupernode() + } + + if item.channelMode, item.isEmpty, let theme = item.theme { + let textNode: ImmediateTextNode + if let current = self.textNode { + textNode = current + } else { + textNode = ImmediateTextNode() + textNode.maximumNumberOfLines = 2 + textNode.textAlignment = .center + self.textNode = textNode + + self.addSubnode(textNode) + } + + let strings = item.context.sharedContext.currentPresentationData.with { $0 }.strings + textNode.attributedText = NSAttributedString(string: strings.Wallpaper_NoWallpaper, font: Font.regular(15.0), textColor: theme.list.itemSecondaryTextColor) + } + + self.setNeedsLayout() + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let item = self.item, !item.isEmpty { + item.interaction.openWallpaper(item.wallpaper) + } + } + } + + func updateSelectionState(animated: Bool) { + if let item = self.item { + let (editing, selectedIds) = item.interaction.selectionState + + if editing && item.editable { + let selected = selectedIds.contains(item.wallpaperId) + + if let selectionNode = self.selectionNode { + selectionNode.updateSelected(selected, animated: animated) + selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + } else { + let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme + let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in + if let strongSelf = self { + strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value) + } + }) + + selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + self.addSubnode(selectionNode) + self.selectionNode = selectionNode + selectionNode.updateSelected(selected, animated: false) + if animated { + selectionNode.animateIn() + } + } + } + else { + if let selectionNode = self.selectionNode { + self.selectionNode = nil + if animated { + selectionNode.animateOut { [weak selectionNode] in + selectionNode?.removeFromSupernode() + } + } else { + selectionNode.removeFromSupernode() + } + } + } + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + if let item = self.item { + self.wallpaperNode.setWallpaper(context: item.context, theme: item.theme, wallpaper: item.wallpaper, isEmpty: item.isEmpty, emojiFile: item.emojiFile, selected: !item.channelMode && item.selected, size: bounds.size, synchronousLoad: false) + self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) + } + self.selectionBorderNode?.frame = CGRect(origin: CGPoint(), size: bounds.size) + + if let textNode = self.textNode { + let textSize = textNode.updateLayout(bounds.size) + textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: floorToScreenPixels((bounds.height - textSize.height) / 2.0) - 18.0), size: textSize) + } + } +} diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift similarity index 67% rename from submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift index 7c69c681d9c..50ba9af0cdd 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift @@ -10,11 +10,14 @@ import TelegramPresentationData import TelegramUIPreferences import MergeLists import ItemListUI +import ItemListPeerActionItem import PresentationDataUtils import AccountContext import SearchBarNode import SearchUI import WallpaperResources +import WallpaperGalleryScreen +import BoostLevelIconComponent struct ThemeGridControllerNodeState: Equatable { var editing: Bool @@ -27,12 +30,14 @@ final class ThemeGridControllerInteraction { let deleteSelectedWallpapers: () -> Void let shareSelectedWallpapers: () -> Void var selectionState: (Bool, Set) = (false, Set()) + var removeWallpaper: () -> Void - init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (ThemeGridControllerEntry.StableId, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void) { + init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (ThemeGridControllerEntry.StableId, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void, removeWallpaper: @escaping () -> Void) { self.openWallpaper = openWallpaper self.toggleWallpaperSelection = toggleWallpaperSelection self.deleteSelectedWallpapers = deleteSelectedWallpapers self.shareSelectedWallpapers = shareSelectedWallpapers + self.removeWallpaper = removeWallpaper } } @@ -43,10 +48,15 @@ struct ThemeGridControllerEntry: Comparable, Identifiable { case gradient([UInt32]) case file(Int64, [UInt32], Int32) case image(String) + case emoticon(String) } var index: Int + var theme: PresentationTheme? var wallpaper: TelegramWallpaper + var isEmpty: Bool = false + var emoji: TelegramMediaFile? + var channelMode: Bool = false var isEditable: Bool var isSelected: Bool @@ -70,11 +80,13 @@ struct ThemeGridControllerEntry: Comparable, Identifiable { } else { return .image("") } + case let .emoticon(emoticon): + return .emoticon(emoticon) } } func item(context: AccountContext, interaction: ThemeGridControllerInteraction) -> ThemeGridControllerItem { - return ThemeGridControllerItem(context: context, wallpaper: self.wallpaper, wallpaperId: self.stableId, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction) + return ThemeGridControllerItem(context: context, theme: self.theme, wallpaper: self.wallpaper, wallpaperId: self.stableId, isEmpty: self.isEmpty, emojiFile: self.emoji, channelMode: self.channelMode, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction) } } @@ -140,6 +152,7 @@ final class ThemeGridControllerNode: ASDisplayNode { } private let context: AccountContext + private let mode: ThemeGridController.Mode private var presentationData: PresentationData private var controllerInteraction: ThemeGridControllerInteraction? @@ -150,19 +163,24 @@ final class ThemeGridControllerNode: ASDisplayNode { private let resetWallpapers: () -> Void var requestDeactivateSearch: (() -> Void)? + var requestWallpaperRemoval: (() -> Void)? let ready = ValuePromise() - private let wallpapersPromise: Promise<[Wallpaper]> + private let wallpapersPromise = Promise<[Wallpaper]>() + private let themesPromise = Promise<[TelegramTheme]>() private var backgroundNode: ASDisplayNode private var separatorNode: ASDisplayNode private var bottomBackgroundNode: ASDisplayNode private var bottomSeparatorNode: ASDisplayNode + private let maskNode: ASImageNode private let colorItemNode: ItemListActionItemNode private var colorItem: ItemListActionItem - private let galleryItemNode: ItemListActionItemNode - private var galleryItem: ItemListActionItem + private let galleryItemNode: ListViewItemNode + private var galleryItem: ItemListItem + private let removeItemNode: ItemListPeerActionItemNode + private var removeItem: ItemListPeerActionItem private let descriptionItemNode: ItemListTextItemNode private var descriptionItem: ItemListTextItem private let resetItemNode: ItemListActionItemNode @@ -192,8 +210,9 @@ final class ThemeGridControllerNode: ASDisplayNode { private var disposable: Disposable? - init(context: AccountContext, presentationData: PresentationData, presentPreviewController: @escaping (WallpaperListSource) -> Void, presentGallery: @escaping () -> Void, presentColors: @escaping () -> Void, emptyStateUpdated: @escaping (Bool) -> Void, deleteWallpapers: @escaping ([TelegramWallpaper], @escaping () -> Void) -> Void, shareWallpapers: @escaping ([TelegramWallpaper]) -> Void, resetWallpapers: @escaping () -> Void, popViewController: @escaping () -> Void) { + init(context: AccountContext, mode: ThemeGridController.Mode, presentationData: PresentationData, presentPreviewController: @escaping (WallpaperListSource) -> Void, presentGallery: @escaping () -> Void, presentColors: @escaping () -> Void, emptyStateUpdated: @escaping (Bool) -> Void, deleteWallpapers: @escaping ([TelegramWallpaper], @escaping () -> Void) -> Void, shareWallpapers: @escaping ([TelegramWallpaper]) -> Void, resetWallpapers: @escaping () -> Void, popViewController: @escaping () -> Void) { self.context = context + self.mode = mode self.presentationData = presentationData self.presentPreviewController = presentPreviewController self.presentGallery = presentGallery @@ -220,16 +239,49 @@ final class ThemeGridControllerNode: ASDisplayNode { self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + self.colorItemNode = ItemListActionItemNode() self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { presentColors() }) - self.galleryItemNode = ItemListActionItemNode() - self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { - presentGallery() + + switch mode { + case .generic: + self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { + presentGallery() + }) + self.galleryItemNode = ItemListActionItemNode() + case .peer: + var requiredCustomWallpaperLevel: Int? + if case let .peer(_, _, _, _, customLevel) = mode { + requiredCustomWallpaperLevel = customLevel + } + + self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Appearance_BoostLevel("\($0)").string) }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { + presentGallery() + }) + self.galleryItemNode = ItemListPeerActionItemNode() + } + + var removeImpl: (() -> Void)? + self.removeItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.list.itemDestructiveColor), title: presentationData.strings.Wallpaper_ChannelRemoveBackground, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .destructive, editing: false, action: { + removeImpl?() }) + self.removeItemNode = ItemListPeerActionItemNode() + self.descriptionItemNode = ItemListTextItemNode() - self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) + + let descriptionText: String + switch mode { + case .generic: + descriptionText = presentationData.strings.Wallpaper_SetCustomBackgroundInfo + case .peer: + descriptionText = presentationData.strings.Wallpaper_ChannelCustomBackgroundInfo + } + self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(descriptionText), sectionId: 0) + self.resetItemNode = ItemListActionItemNode() self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { resetWallpapers() @@ -240,9 +292,6 @@ final class ThemeGridControllerNode: ASDisplayNode { self.currentState = ThemeGridControllerNodeState(editing: false, selectedIds: Set()) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) - let wallpapersPromise = Promise<[Wallpaper]>() - self.wallpapersPromise = wallpapersPromise - let deletedWallpaperIdsValue = Atomic>(value: Set()) let deletedWallpaperIdsPromise = ValuePromise>(Set()) @@ -255,23 +304,32 @@ final class ThemeGridControllerNode: ASDisplayNode { self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.gridNode.addSubnode(self.backgroundNode) - self.gridNode.addSubnode(self.separatorNode) self.gridNode.addSubnode(self.bottomBackgroundNode) - self.gridNode.addSubnode(self.bottomSeparatorNode) - self.gridNode.addSubnode(self.colorItemNode) +// self.gridNode.addSubnode(self.bottomSeparatorNode) + if case .generic = mode { + self.gridNode.addSubnode(self.colorItemNode) + } self.gridNode.addSubnode(self.galleryItemNode) + if case let .peer(_, _, wallpaper, _, _) = mode, let wallpaper, !wallpaper.isEmoticon { + self.gridNode.addSubnode(self.removeItemNode) + } self.gridNode.addSubnode(self.descriptionItemNode) - self.gridNode.addSubnode(self.resetItemNode) - self.gridNode.addSubnode(self.resetDescriptionItemNode) + + if case .generic = mode { + self.gridNode.addSubnode(self.resetItemNode) + self.gridNode.addSubnode(self.resetDescriptionItemNode) + } self.addSubnode(self.gridNode) + self.gridNode.addSubnode(self.maskNode) + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true) let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil) let interaction = ThemeGridControllerInteraction(openWallpaper: { [weak self] wallpaper in if let strongSelf = self, !strongSelf.currentState.editing { let entries = previousEntries.with { $0 } if let entries = entries, !entries.isEmpty { - let wallpapers = entries.map { $0.wallpaper } + let wallpapers = entries.map { $0.wallpaper }.filter { !$0.isColorOrGradient } var options = WallpaperPresentationOptions() if wallpaper == strongSelf.presentationData.chatWallpaper, let settings = wallpaper.settings { @@ -336,81 +394,123 @@ final class ThemeGridControllerNode: ASDisplayNode { if let strongSelf = self, let entries = entries { shareWallpapers(selectedWallpapers(entries: entries, state: strongSelf.currentState)) } + }, removeWallpaper: { [weak self] in + if let self { + self.requestWallpaperRemoval?() + } }) self.controllerInteraction = interaction - let transition = combineLatest(self.wallpapersPromise.get(), deletedWallpaperIdsPromise.get(), context.sharedContext.presentationData) - |> map { wallpapers, deletedWallpaperIds, presentationData -> (ThemeGridEntryTransition, Bool) in + let transition = combineLatest(self.wallpapersPromise.get(), self.themesPromise.get(), deletedWallpaperIdsPromise.get(), context.sharedContext.presentationData) + |> map { wallpapers, themes, deletedWallpaperIds, presentationData -> (ThemeGridEntryTransition, Bool) in var entries: [ThemeGridControllerEntry] = [] var index: Int = 0 - - entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, isEditable: false, isSelected: true), at: 0) - index += 1 - var defaultWallpaper: TelegramWallpaper? - if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) { - let entry = ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, isEditable: false, isSelected: false) - if !entries.contains(where: { $0.stableId == entry.stableId }) { - defaultWallpaper = presentationData.theme.chat.defaultWallpaper - entries.insert(entry, at: index) + if !themes.isEmpty { + var selectedWallpaper: TelegramWallpaper? + if case let .peer(_, _, wallpaper, _, _) = mode { + selectedWallpaper = wallpaper + } + + if let selectedWallpaper, !selectedWallpaper.isEmoticon { + entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: selectedWallpaper, channelMode: true, isEditable: false, isSelected: true)) + } else { + let emojiFile = context.animatedEmojiStickers["❌"]?.first?.file + + entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: .color(0), isEmpty: true, emoji: emojiFile, channelMode: true, isEditable: false, isSelected: selectedWallpaper == nil)) + } + index += 1 + + for theme in themes { + guard let wallpaper = theme.settings?.first?.wallpaper, let themeEmoticon = theme.emoticon else { + continue + } + + var updatedWallpaper = wallpaper + if let settings = wallpaper.settings { + var updatedSettings = settings + updatedSettings.emoticon = themeEmoticon + updatedWallpaper = wallpaper.withUpdatedSettings(updatedSettings) + } + + var isSelected = false + if let selectedWallpaper, case let .emoticon(emoticon) = selectedWallpaper, emoticon.strippedEmoji == themeEmoticon.strippedEmoji { + isSelected = true + } + + let emoji = context.animatedEmojiStickers[themeEmoticon] + entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: updatedWallpaper, emoji: emoji?.first?.file, channelMode: true, isEditable: false, isSelected: isSelected)) index += 1 } - } - - var sortedWallpapers: [TelegramWallpaper] = [] - if presentationData.theme.overallDarkAppearance { - var localWallpapers: [TelegramWallpaper] = [] - var darkWallpapers: [TelegramWallpaper] = [] - for wallpaper in wallpapers { - if wallpaper.isLocal { - localWallpapers.append(wallpaper.wallpaper) - } else { - if case let .file(file) = wallpaper.wallpaper, file.isDark { - darkWallpapers.append(wallpaper.wallpaper) + } else { + entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, emoji: nil, isEditable: false, isSelected: true), at: 0) + index += 1 + + var defaultWallpaper: TelegramWallpaper? + if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) { + let entry = ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, emoji: nil, isEditable: false, isSelected: false) + if !entries.contains(where: { $0.stableId == entry.stableId }) { + defaultWallpaper = presentationData.theme.chat.defaultWallpaper + entries.insert(entry, at: index) + index += 1 + } + } + + var sortedWallpapers: [TelegramWallpaper] = [] + if presentationData.theme.overallDarkAppearance { + var localWallpapers: [TelegramWallpaper] = [] + var darkWallpapers: [TelegramWallpaper] = [] + for wallpaper in wallpapers { + if wallpaper.isLocal { + localWallpapers.append(wallpaper.wallpaper) } else { - sortedWallpapers.append(wallpaper.wallpaper) + if case let .file(file) = wallpaper.wallpaper, file.isDark { + darkWallpapers.append(wallpaper.wallpaper) + } else { + sortedWallpapers.append(wallpaper.wallpaper) + } } } - } - sortedWallpapers = localWallpapers + darkWallpapers + sortedWallpapers - } else { - sortedWallpapers = wallpapers.map(\.wallpaper) - } - - if let builtinIndex = sortedWallpapers.firstIndex(where: { wallpaper in - if case .builtin = wallpaper { - return true + sortedWallpapers = localWallpapers + darkWallpapers + sortedWallpapers } else { - return false - } - }) { - sortedWallpapers[builtinIndex] = defaultBuiltinWallpaper(data: .legacy, colors: legacyBuiltinWallpaperGradientColors.map(\.rgb)) - } - - for wallpaper in sortedWallpapers { - if case let .file(file) = wallpaper, (wallpaper.isPattern && file.settings.colors.isEmpty) { - continue - } - let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) - var isDefault = false - if let defaultWallpaper = defaultWallpaper, defaultWallpaper.isBasicallyEqual(to: wallpaper) { - isDefault = true - } - var isEditable = true - if case .builtin = wallpaper { - isEditable = false + sortedWallpapers = wallpapers.map(\.wallpaper) } - if isDefault || presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) { - isEditable = false + + if let builtinIndex = sortedWallpapers.firstIndex(where: { wallpaper in + if case .builtin = wallpaper { + return true + } else { + return false + } + }) { + sortedWallpapers[builtinIndex] = defaultBuiltinWallpaper(data: .legacy, colors: legacyBuiltinWallpaperGradientColors.map(\.rgb)) } - if !selected && !isDefault { - let entry = ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false) - if deletedWallpaperIds.contains(entry.stableId) { + + for wallpaper in sortedWallpapers { + if case let .file(file) = wallpaper, (wallpaper.isPattern && file.settings.colors.isEmpty) { continue } - if !entries.contains(where: { $0.stableId == entry.stableId }) { - entries.append(entry) - index += 1 + let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) + var isDefault = false + if let defaultWallpaper = defaultWallpaper, defaultWallpaper.isBasicallyEqual(to: wallpaper) { + isDefault = true + } + var isEditable = true + if case .builtin = wallpaper { + isEditable = false + } + if isDefault || presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) { + isEditable = false + } + if !selected && !isDefault { + let entry = ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false) + if deletedWallpaperIds.contains(entry.stableId) { + continue + } + if !entries.contains(where: { $0.stableId == entry.stableId }) { + entries.append(entry) + index += 1 + } } } } @@ -423,6 +523,10 @@ final class ThemeGridControllerNode: ASDisplayNode { strongSelf.enqueueTransition(transition) } }) + + removeImpl = { [weak self] in + self?.controllerInteraction?.removeWallpaper() + } self.updateWallpapers() } @@ -449,6 +553,8 @@ final class ThemeGridControllerNode: ASDisplayNode { highlightedNode = strongSelf.galleryItemNode } else if strongSelf.resetItemNode.frame.contains(point) { highlightedNode = strongSelf.resetItemNode + } else if strongSelf.removeItemNode.frame.contains(point) { + highlightedNode = strongSelf.removeItemNode } } @@ -458,6 +564,7 @@ final class ThemeGridControllerNode: ASDisplayNode { strongSelf.colorItemNode.setHighlighted(false, at: CGPoint(), animated: true) strongSelf.galleryItemNode.setHighlighted(false, at: CGPoint(), animated: true) strongSelf.resetItemNode.setHighlighted(false, at: CGPoint(), animated: true) + strongSelf.removeItemNode.setHighlighted(false, at: CGPoint(), animated: true) } } } @@ -475,11 +582,14 @@ final class ThemeGridControllerNode: ASDisplayNode { let (resetLayout, resetApply) = makeResetLayout(strongSelf.resetItem, params, ItemListNeighbors(top: .none, bottom: .sameSection(alwaysPlain: true))) let (resetDescriptionLayout, resetDescriptionApply) = makeResetDescriptionLayout(strongSelf.resetDescriptionItem, params, ItemListNeighbors(top: .none, bottom: .none)) - resetApply() + resetApply(false) resetDescriptionApply() transition.updateFrame(node: strongSelf.resetItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: gridLayout.contentSize.height + 35.0), size: resetLayout.contentSize)) 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 sideInset = strongSelf.leftOverlayNode.frame.maxX + strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel + 4.0), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 6.0)) } } } @@ -493,9 +603,15 @@ final class ThemeGridControllerNode: ASDisplayNode { if self.colorItemNode.frame.contains(location) { self.colorItem.action() } else if self.galleryItemNode.frame.contains(location) { - self.galleryItem.action() + if let galleryItem = self.galleryItem as? ItemListActionItem { + galleryItem.action() + } else if let galleryItem = self.galleryItem as? ItemListPeerActionItem { + galleryItem.action?() + } } else if self.resetItemNode.frame.contains(location) { self.resetItem.action() + } else if self.removeItemNode.frame.contains(location) { + self.removeItem.action?() } default: break @@ -507,31 +623,38 @@ final class ThemeGridControllerNode: ASDisplayNode { } func updateWallpapers() { - self.wallpapersPromise.set(combineLatest(queue: .mainQueue(), - telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network), - self.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.wallapersState]) - ) - |> map { remoteWallpapers, sharedData -> [Wallpaper] in - let localState = sharedData.entries[SharedDataKeys.wallapersState]?.get(WallpapersState.self) ?? WallpapersState.default + switch self.mode { + case .generic: + self.wallpapersPromise.set(combineLatest(queue: .mainQueue(), + telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network), + self.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.wallapersState]) + ) + |> map { remoteWallpapers, sharedData -> [Wallpaper] in + let localState = sharedData.entries[SharedDataKeys.wallapersState]?.get(WallpapersState.self) ?? WallpapersState.default - var wallpapers: [Wallpaper] = [] - for wallpaper in localState.wallpapers { - if !wallpapers.contains(where: { - $0.wallpaper.isBasicallyEqual(to: wallpaper) - }) { - wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: true)) + var wallpapers: [Wallpaper] = [] + for wallpaper in localState.wallpapers { + if !wallpapers.contains(where: { + $0.wallpaper.isBasicallyEqual(to: wallpaper) + }) { + wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: true)) + } } - } - for wallpaper in remoteWallpapers { - if !wallpapers.contains(where: { - $0.wallpaper.isBasicallyEqual(to: wallpaper) - }) { - wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: false)) + for wallpaper in remoteWallpapers { + if !wallpapers.contains(where: { + $0.wallpaper.isBasicallyEqual(to: wallpaper) + }) { + wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: false)) + } } - } - return wallpapers - }) + return wallpapers + }) + self.themesPromise.set(.single([])) + case let .peer(_, themes, _, _, _): + self.themesPromise.set(.single(themes)) + self.wallpapersPromise.set(.single([])) + } } func updatePresentationData(_ presentationData: PresentationData) { @@ -550,9 +673,25 @@ final class ThemeGridControllerNode: ASDisplayNode { self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentColors() }) - self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in - self?.presentGallery() - }) + + switch self.mode { + case .generic: + self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in + self?.presentGallery() + }) + case .peer: + var requiredCustomWallpaperLevel: Int? + if case let .peer(_, _, _, _, customLevel) = mode { + requiredCustomWallpaperLevel = customLevel + } + self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.Channel_Appearance_BoostLevel("\($0)").string) }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { [weak self] in + self?.presentGallery() + }) + self.removeItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.list.itemDestructiveColor), title: presentationData.strings.Wallpaper_ChannelRemoveBackground, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .destructive, editing: false, action: { [weak self] in + self?.controllerInteraction?.removeWallpaper() + }) + } + self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0) self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.resetWallpapers() @@ -620,28 +759,33 @@ final class ThemeGridControllerNode: ASDisplayNode { var scrollIndicatorInsets = insets - let minSpacing: CGFloat = 8.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: 108.0, height: 230.0) + referenceImageSize = CGSize(width: 112.0, height: 150.0) } else { referenceImageSize = CGSize(width: 91.0, height: 161.0) } - let imageCount = Int((layout.size.width - insets.left - insets.right - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) - let spacing = floor((layout.size.width - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) + + 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 makeColorLayout = self.colorItemNode.asyncLayout() - let makeGalleryLayout = self.galleryItemNode.asyncLayout() + let makeGalleryLayout = (self.galleryItemNode as? ItemListActionItemNode)?.asyncLayout() + let makeGalleryIconLayout = (self.galleryItemNode as? ItemListPeerActionItemNode)?.asyncLayout() + let makeRemoveLayout = self.removeItemNode.asyncLayout() let makeDescriptionLayout = self.descriptionItemNode.asyncLayout() var listInsets = insets 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 - + listInsets.left = sideInset + listInsets.right = sideInset if self.leftOverlayNode.supernode == nil { self.gridNode.addSubnode(self.leftOverlayNode) } @@ -657,33 +801,75 @@ final class ThemeGridControllerNode: ASDisplayNode { } } + var isChannel = false + var hasCustomWallpaper = false + if case let .peer(_, _, wallpaper, _, _) = self.mode { + isChannel = true + if let wallpaper, !wallpaper.isEmoticon { + hasCustomWallpaper = true + } + } + let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: listInsets.left, rightInset: listInsets.right, availableHeight: layout.size.height) let (colorLayout, colorApply) = makeColorLayout(self.colorItem, params, ItemListNeighbors(top: .none, bottom: .sameSection(alwaysPlain: false))) - let (galleryLayout, galleryApply) = makeGalleryLayout(self.galleryItem, params, ItemListNeighbors(top: .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: true))) + let (galleryLayout, galleryApply): (ListViewItemNodeLayout, (Bool) -> Void) + if let makeGalleryIconLayout, let galleryItem = self.galleryItem as? ItemListPeerActionItem { + (galleryLayout, galleryApply) = makeGalleryIconLayout(galleryItem, params, ItemListNeighbors(top: isChannel ? .none : .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: !hasCustomWallpaper))) + } else if let makeGalleryLayout, let galleryItem = self.galleryItem as? ItemListActionItem { + (galleryLayout, galleryApply) = makeGalleryLayout(galleryItem, params, ItemListNeighbors(top: isChannel ? .none : .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: false))) + } else { + fatalError() + } + let (removeLayout, removeApply) = makeRemoveLayout(self.removeItem, params, ItemListNeighbors(top: .sameSection(alwaysPlain: false), bottom: .none)) + let (descriptionLayout, descriptionApply) = makeDescriptionLayout(self.descriptionItem, params, ItemListNeighbors(top: .none, bottom: .none)) - colorApply() - galleryApply() + + colorApply(false) + galleryApply(false) + removeApply(false) descriptionApply() let buttonTopInset: CGFloat = 32.0 let buttonHeight: CGFloat = 44.0 - let buttonBottomInset: CGFloat = descriptionLayout.contentSize.height + 17.0 + var buttonBottomInset: CGFloat = descriptionLayout.contentSize.height + 17.0 + if hasCustomWallpaper { + buttonBottomInset = 17.0 + } - let buttonInset: CGFloat = buttonTopInset + buttonHeight * 2.0 + buttonBottomInset + var buttonInset: CGFloat = buttonTopInset + buttonHeight + buttonBottomInset + if !isChannel || hasCustomWallpaper { + buttonInset += buttonHeight + } 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 + 500.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.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset), size: colorLayout.contentSize)) - transition.updateFrame(node: self.galleryItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset + colorLayout.contentSize.height), size: galleryLayout.contentSize)) - transition.updateFrame(node: self.descriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height), size: descriptionLayout.contentSize)) + var originY = -buttonOffset + buttonTopInset + if !isChannel { + transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: colorLayout.contentSize)) + originY += colorLayout.contentSize.height + } + transition.updateFrame(node: self.galleryItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: galleryLayout.contentSize)) + originY += galleryLayout.contentSize.height + + if hasCustomWallpaper { + self.descriptionItemNode.isHidden = true + self.removeItemNode.isHidden = false + + transition.updateFrame(node: self.removeItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: removeLayout.contentSize)) + } else { + self.descriptionItemNode.isHidden = false + self.removeItemNode.isHidden = true + transition.updateFrame(node: self.descriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: descriptionLayout.contentSize)) + } - self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height) - self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height) + self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height + 10000.0) + 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 if self.currentState.editing { let panelHeight: CGFloat @@ -747,10 +933,12 @@ final class ThemeGridControllerNode: ASDisplayNode { let makeResetDescriptionLayout = self.resetDescriptionItemNode.asyncLayout() let (resetDescriptionLayout, _) = makeResetDescriptionLayout(self.resetDescriptionItem, params, ItemListNeighbors(top: .none, bottom: .none)) - insets.bottom += buttonHeight + 35.0 + resetDescriptionLayout.contentSize.height + 32.0 + if !isChannel { + insets.bottom += buttonHeight + 35.0 + resetDescriptionLayout.contentSize.height + 32.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: insets, 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.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 }) if !hadValidLayout { self.dequeueTransitions() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/ThemeGridSearchColorsItem.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/ThemeGridSearchContentNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/ThemeGridSearchItem.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift diff --git a/submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/ThemeGridSelectionPanelNode.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperSearchRecentQueries.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift similarity index 100% rename from submodules/SettingsUI/Sources/Themes/WallpaperSearchRecentQueries.swift rename to submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift new file mode 100644 index 00000000000..fafe52e6e90 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift @@ -0,0 +1,377 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import MediaResources +import LocalMediaResources +import TelegramUIPreferences +import AccountContext +import LegacyComponents +import WallpaperGalleryScreen +import ImageBlur + +public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, completion: @escaping () -> Void) { + var imageSignal: Signal + switch wallpaper { + case let .wallpaper(wallpaper, _): + switch wallpaper { + case let .file(file): + if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() + } + case let .image(representations, _): + for representation in representations { + let resource = representation.resource + if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() + } + } + default: + break + } + imageSignal = .complete() + completion() + case let .asset(asset): + imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false) + |> filter { value in + return !(value?.1 ?? true) + } + |> mapToSignal { result -> Signal in + if let result = result { + return .single(result.0) + } else { + return .complete() + } + } + case let .contextResult(result): + var imageResource: TelegramMediaResource? + switch result { + case let .externalReference(externalReference): + if let content = externalReference.content { + imageResource = content.resource + } + case let .internalReference(internalReference): + if let image = internalReference.image { + if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { + imageResource = imageRepresentation.resource + } + } + } + + if let imageResource = imageResource { + imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + |> mapToSignal { path -> Signal in + if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { + return .single(image) + } else { + return .complete() + } + } + } else { + imageSignal = .complete() + } + } + + if let editedImage { + imageSignal = .single(editedImage) + } + + let _ = (imageSignal + |> map { image -> UIImage in + var croppedImage = UIImage() + + let finalCropRect: CGRect + if let cropRect = cropRect { + finalCropRect = cropRect + } else { + let screenSize = TGScreenSize() + let fittedSize = TGScaleToFit(screenSize, image.size) + finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height) + } + croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true) + + let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0)) + let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) + + if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered + let accountManager = context.sharedContext.accountManager + let account = context.account + let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in + var resource: MediaResource? + if case let .image(representations, _) = wallpaper, let representation = largestImageRepresentation(representations) { + resource = representation.resource + } else if case let .file(file) = wallpaper { + resource = file.file.resource + } + + if let resource = resource { + let _ = accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) + let _ = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) + } + + let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + 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) + })).start() + } + + let apply: () -> Void = { + let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil) + let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) + updateWallpaper(wallpaper) + DispatchQueue.main.async { + completion() + } + } + + if mode.contains(.blur) { + let representation = CachedBlurredWallpaperRepresentation() + let _ = context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start() + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start(completed: { + apply() + }) + } else { + apply() + } + } + return croppedImage + }).start() +} + +public func getTemporaryCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?) -> Signal { + var imageSignal: Signal + switch wallpaper { + case .wallpaper: + fatalError() + case let .asset(asset): + imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false) + |> filter { value in + return !(value?.1 ?? true) + } + |> mapToSignal { result -> Signal in + if let result = result { + return .single(result.0) + } else { + return .complete() + } + } + case let .contextResult(result): + var imageResource: TelegramMediaResource? + switch result { + case let .externalReference(externalReference): + if let content = externalReference.content { + imageResource = content.resource + } + case let .internalReference(internalReference): + if let image = internalReference.image { + if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { + imageResource = imageRepresentation.resource + } + } + } + + if let imageResource = imageResource { + imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + |> mapToSignal { path -> Signal in + if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { + return .single(image) + } else { + return .complete() + } + } + } else { + imageSignal = .complete() + } + } + + if let editedImage { + imageSignal = .single(editedImage) + } + + return imageSignal + |> map { image -> TelegramWallpaper? in + var croppedImage = UIImage() + + let finalCropRect: CGRect + if let cropRect = cropRect { + finalCropRect = cropRect + } else { + let screenSize = TGScreenSize() + let fittedSize = TGScaleToFit(screenSize, image.size) + finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height) + } + croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true) + + let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0)) + let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) + + if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() + + var intensity: Int32? + if let brightness { + intensity = max(0, min(100, Int32(brightness * 100.0))) + } + + let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity) + let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) + + return temporaryWallpaper + } + return nil + } +} + +public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, peerId: PeerId, forBoth: Bool, completion: @escaping () -> Void) { + var imageSignal: Signal + switch wallpaper { + case let .wallpaper(wallpaper, _): + imageSignal = .complete() + switch wallpaper { + case let .file(file): + if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) { + context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true) + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() + + imageSignal = .single(image) + } + case let .image(representations, _): + for representation in representations { + let resource = representation.resource + if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() + } + } + default: + break + } + completion() + case let .asset(asset): + imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false) + |> filter { value in + return !(value?.1 ?? true) + } + |> mapToSignal { result -> Signal in + if let result = result { + return .single(result.0) + } else { + return .complete() + } + } + case let .contextResult(result): + var imageResource: TelegramMediaResource? + switch result { + case let .externalReference(externalReference): + if let content = externalReference.content { + imageResource = content.resource + } + case let .internalReference(internalReference): + if let image = internalReference.image { + if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { + imageResource = imageRepresentation.resource + } + } + } + + if let imageResource = imageResource { + imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + |> mapToSignal { path -> Signal in + if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { + return .single(image) + } else { + return .complete() + } + } + } else { + imageSignal = .complete() + } + } + + if let editedImage { + imageSignal = .single(editedImage) + } + + let _ = (imageSignal + |> map { image -> UIImage in + var croppedImage = UIImage() + + let finalCropRect: CGRect + if let cropRect = cropRect { + finalCropRect = cropRect + } else { + let screenSize = TGScreenSize() + let fittedSize = TGScaleToFit(screenSize, image.size) + finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height) + } + croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true) + + let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0)) + let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0) + + if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { + let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + + let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() + + var intensity: Int32? + if let brightness { + intensity = max(0, min(100, Int32(brightness * 100.0))) + } + + Queue.mainQueue().after(0.05) { + let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity) + let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings) + + context.account.pendingPeerMediaUploadManager.add(peerId: peerId, content: .wallpaper(wallpaper: temporaryWallpaper, forBoth: forBoth)) + + Queue.mainQueue().after(0.05) { + completion() + } + } + } + return croppedImage + }).start() +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a14f0398f93..7eb14f88d4a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1066,7 +1066,7 @@ final class ShareWithPeersScreenComponent: Component { rightAccessory: accessory, selectionState: .none, hasNext: i < peers.count - 1, - action: { [weak self] peer in + action: { [weak self] peer, _, _ in guard let self, let component = self.component else { return } @@ -1414,7 +1414,7 @@ final class ShareWithPeersScreenComponent: Component { presence: stateValue.presences[peer.id], selectionState: .editing(isSelected: isSelected, isTinted: false), hasNext: true, - action: { [weak self] peer in + action: { [weak self] peer, _, _ in guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } @@ -2143,7 +2143,7 @@ final class ShareWithPeersScreenComponent: Component { presence: nil, selectionState: .editing(isSelected: false, isTinted: false), hasNext: true, - action: { _ in + action: { _, _, _ in } )), environment: {}, @@ -2578,119 +2578,123 @@ final class ShareWithPeersScreenComponent: Component { } } - let presentAlert: ([String]) -> Void = { usernames in - let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", ")) - let alertController = textAlertController( - context: component.context, - forceTheme: defaultDarkColorPresentationTheme, - title: environment.strings.Story_Privacy_MentionRestrictedTitle, - text: environment.strings.Story_Privacy_MentionRestrictedText(usernamesString).string, - actions: [ - TextAlertAction(type: .defaultAction, title: environment.strings.Story_Privacy_MentionRestrictedProceed, action: { - proceed() - }), - TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}) - ], - actionLayout: .vertical - ) - controller.present(alertController, in: .window(.root)) - } - - func matchingUsername(user: TelegramUser, usernames: Set) -> String? { - for username in user.usernames { - if usernames.contains(username.username) { - return username.username - } + if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel { + proceed() + } else { + let presentAlert: ([String]) -> Void = { usernames in + let usernamesString = String(usernames.map { "@\($0)" }.joined(separator: ", ")) + let alertController = textAlertController( + context: component.context, + forceTheme: defaultDarkColorPresentationTheme, + title: environment.strings.Story_Privacy_MentionRestrictedTitle, + text: environment.strings.Story_Privacy_MentionRestrictedText(usernamesString).string, + actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Story_Privacy_MentionRestrictedProceed, action: { + proceed() + }), + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + controller.present(alertController, in: .window(.root)) } - if let username = user.username { - if usernames.contains(username) { - return username + + func matchingUsername(user: TelegramUser, usernames: Set) -> String? { + for username in user.usernames { + if usernames.contains(username.username) { + return username.username + } } + if let username = user.username { + if usernames.contains(username) { + return username + } + } + return nil } - return nil - } - - let context = component.context - let selectedPeerIds = self.selectedPeers - - if case .stories = component.stateContext.subject { - if component.mentions.isEmpty { - proceed() - } else if case .nobody = base { - if selectedPeerIds.isEmpty { - presentAlert(component.mentions) - } else { - let _ = (context.account.postbox.transaction { transaction in - var filteredMentions = Set(component.mentions) - for peerId in selectedPeerIds { - if let peer = transaction.getPeer(peerId) { - if let user = peer as? TelegramUser { - if let username = matchingUsername(user: user, usernames: filteredMentions) { - filteredMentions.remove(username) - } - } else { - if let username = peer.addressName { - filteredMentions.remove(username) + + let context = component.context + let selectedPeerIds = self.selectedPeers + + if case .stories = component.stateContext.subject { + if component.mentions.isEmpty { + proceed() + } else if case .nobody = base { + if selectedPeerIds.isEmpty { + presentAlert(component.mentions) + } else { + let _ = (context.account.postbox.transaction { transaction in + var filteredMentions = Set(component.mentions) + for peerId in selectedPeerIds { + if let peer = transaction.getPeer(peerId) { + if let user = peer as? TelegramUser { + if let username = matchingUsername(user: user, usernames: filteredMentions) { + filteredMentions.remove(username) + } + } else { + if let username = peer.addressName { + filteredMentions.remove(username) + } } } } + return Array(filteredMentions) + } + |> deliverOnMainQueue).start(next: { mentions in + if mentions.isEmpty { + proceed() + } else { + presentAlert(mentions) + } + }) + } + } else if case .contacts = base { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) + |> map { contacts -> [String] in + var filteredMentions = Set(component.mentions) + let peers = contacts.peers + for peer in peers { + if selectedPeerIds.contains(peer.id) { + continue + } + if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) { + filteredMentions.remove(username) + } } return Array(filteredMentions) } - |> deliverOnMainQueue).start(next: { mentions in + |> deliverOnMainQueue).start(next: { mentions in if mentions.isEmpty { proceed() } else { presentAlert(mentions) } }) - } - } else if case .contacts = base { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) - |> map { contacts -> [String] in - var filteredMentions = Set(component.mentions) - let peers = contacts.peers - for peer in peers { - if selectedPeerIds.contains(peer.id) { - continue - } - if case let .user(user) = peer, let username = matchingUsername(user: user, usernames: filteredMentions) { - filteredMentions.remove(username) + } else if case .closeFriends = base { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) + |> map { contacts -> [String] in + var filteredMentions = Set(component.mentions) + let peers = contacts.peers + for peer in peers { + if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) { + filteredMentions.remove(username) + } } + return Array(filteredMentions) } - return Array(filteredMentions) - } - |> deliverOnMainQueue).start(next: { mentions in - if mentions.isEmpty { - proceed() - } else { - presentAlert(mentions) - } - }) - } else if case .closeFriends = base { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) - |> map { contacts -> [String] in - var filteredMentions = Set(component.mentions) - let peers = contacts.peers - for peer in peers { - if case let .user(user) = peer, user.flags.contains(.isCloseFriend), let username = matchingUsername(user: user, usernames: filteredMentions) { - filteredMentions.remove(username) + |> deliverOnMainQueue).start(next: { mentions in + if mentions.isEmpty { + proceed() + } else { + presentAlert(mentions) } - } - return Array(filteredMentions) + }) + } else { + proceed() } - |> deliverOnMainQueue).start(next: { mentions in - if mentions.isEmpty { - proceed() - } else { - presentAlert(mentions) - } - }) } else { proceed() } - } else { - proceed() } } )), diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index aa411a98e33..28cd361f2fe 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2564,7 +2564,7 @@ final class StorageUsageScreenComponent: Component { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( @@ -2667,7 +2667,7 @@ final class StorageUsageScreenComponent: Component { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index 922c35d720a..c1f065c7f12 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -96,6 +96,7 @@ public final class ForwardInfoPanelComponent: Component { iconView.alpha = 0.55 iconView.tintColor = .white self.addSubview(iconView) + self.iconView = iconView } if let image = iconView.image { iconView.frame = CGRect(origin: CGPoint(x: sideInset + UIScreenPixel, y: 5.0), size: image.size) diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index e6d82851b0e..0ad1d287a5c 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/ContextUI", "//submodules/TextFormat", + "//submodules/PhotoResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index c2d5ebb1181..c23e8cae70b 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -18,9 +18,12 @@ import EmojiStatusComponent import ContextUI import EmojiTextAttachmentView import TextFormat +import PhotoResources private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) +private let repostIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderRepost"), color: .white)?.withRenderingMode(.alwaysTemplate) +private let forwardIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderForward"), color: .white)?.withRenderingMode(.alwaysTemplate) private let checkImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)?.withRenderingMode(.alwaysTemplate) private let disclosureImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -51,6 +54,8 @@ public final class PeerListItemComponent: Component { public enum SubtitleAccessory: Equatable { case none case checks + case repost + case forward } public enum RightAccessory: Equatable { @@ -105,11 +110,13 @@ public final class PeerListItemComponent: Component { let presence: EnginePeer.Presence? let rightAccessory: RightAccessory let reaction: Reaction? + let story: EngineStoryItem? + let message: EngineMessage? let selectionState: SelectionState let selectionPosition: SelectionPosition let isEnabled: Bool let hasNext: Bool - let action: (EnginePeer) -> Void + let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let openStories: ((EnginePeer, AvatarNode) -> Void)? @@ -127,11 +134,13 @@ public final class PeerListItemComponent: Component { presence: EnginePeer.Presence?, rightAccessory: RightAccessory = .none, reaction: Reaction? = nil, + story: EngineStoryItem? = nil, + message: EngineMessage? = nil, selectionState: SelectionState, selectionPosition: SelectionPosition = .left, isEnabled: Bool = true, hasNext: Bool, - action: @escaping (EnginePeer) -> Void, + action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, openStories: ((EnginePeer, AvatarNode) -> Void)? = nil ) { @@ -148,6 +157,8 @@ public final class PeerListItemComponent: Component { self.presence = presence self.rightAccessory = rightAccessory self.reaction = reaction + self.story = story + self.message = message self.selectionState = selectionState self.selectionPosition = selectionPosition self.isEnabled = isEnabled @@ -197,6 +208,12 @@ public final class PeerListItemComponent: Component { if lhs.reaction != rhs.reaction { return false } + if lhs.story != rhs.story { + return false + } + if lhs.message != rhs.message { + return false + } if lhs.selectionState != rhs.selectionState { return false } @@ -232,6 +249,9 @@ public final class PeerListItemComponent: Component { private var file: TelegramMediaFile? private var fileDisposable: Disposable? + private var imageButtonView: HighlightTrackingButton? + public private(set) var imageNode: TransformImageNode? + private var component: PeerListItemComponent? private weak var state: EmptyComponentState? @@ -330,7 +350,7 @@ public final class PeerListItemComponent: Component { guard let component = self.component, let peer = component.peer else { return } - component.action(peer) + component.action(peer, component.message?.id, self.imageNode?.view) } @objc private func avatarButtonPressed() { @@ -380,6 +400,10 @@ public final class PeerListItemComponent: Component { } } + public func updateIsPreviewing(isPreviewing: Bool) { + self.imageNode?.isHidden = isPreviewing + } + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let previousComponent = self.component @@ -472,6 +496,9 @@ public final class PeerListItemComponent: Component { if component.reaction != nil || component.rightAccessory != .none { rightInset += 32.0 } + if component.story != nil { + rightInset += 40.0 + } var avatarLeftInset: CGFloat = component.sideInset + 10.0 @@ -574,7 +601,7 @@ public final class PeerListItemComponent: Component { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { statusIcon = .text(color: component.theme.chat.message.incoming.scamColor, string: component.strings.Message_FakeAccount.uppercased()) - } else if case let .user(user) = peer, let emojiStatus = user.emojiStatus { + } else if let emojiStatus = peer.emojiStatus { statusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(2)) } else if peer.isVerified { statusIcon = .verified(fillColor: component.theme.list.itemCheckColors.fillColor, foregroundColor: component.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) @@ -696,24 +723,37 @@ public final class PeerListItemComponent: Component { if let labelView = self.label.view { var iconLabelOffset: CGFloat = 0.0 - if case .checks = component.subtitleAccessory { + if case .none = component.subtitleAccessory { + if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + } + } else { let iconView: UIImageView if let current = self.iconView { iconView = current } else { - iconView = UIImageView(image: readIconImage) - iconView.tintColor = component.theme.list.itemSecondaryTextColor + var image: UIImage? + var color: UIColor = component.theme.list.itemSecondaryTextColor + if case .checks = component.subtitleAccessory { + image = readIconImage + } else if case .repost = component.subtitleAccessory { + image = repostIconImage + color = UIColor(rgb: 0x34c759) + } else if case .forward = component.subtitleAccessory { + image = forwardIconImage + color = UIColor(rgb: 0x34c759) + } + iconView = UIImageView(image: image) + iconView.tintColor = color self.iconView = iconView self.containerButton.addSubview(iconView) } if let image = iconView.image { iconLabelOffset = image.size.width + 4.0 - transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 3.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size)) + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing + 2.0 + floor((labelSize.height - image.size.height) * 0.5)), size: image.size)) } - } else if let iconView = self.iconView { - self.iconView = nil - iconView.removeFromSuperview() } if labelView.superview == nil { @@ -825,6 +865,80 @@ public final class PeerListItemComponent: Component { transition.setFrame(layer: reactionLayer, frame: adjustedIconFrame) } + + var mediaReference: AnyMediaReference? + if let peer = component.peer, let peerReference = PeerReference(peer._asPeer()) { + if let story = component.story { + mediaReference = .story(peer: peerReference, id: story.id, media: story.media._asMedia()) + } else if let message = component.message { + var selectedMedia: Media? + for media in message.media { + if let image = media as? TelegramMediaImage { + selectedMedia = image + } else if let file = media as? TelegramMediaFile { + selectedMedia = file + } + } + if let media = selectedMedia { + mediaReference = .message(message: MessageReference(message._asMessage()), media: media) + } + } + } + + if let peer = component.peer, let mediaReference { + let contentImageSize = CGSize(width: 30.0, height: 42.0) + var dimensions: CGSize? + if let imageMedia = mediaReference.media as? TelegramMediaImage { + dimensions = largestRepresentationForPhoto(imageMedia)?.dimensions.cgSize + } else if let imageMedia = mediaReference.media as? TelegramMediaFile { + dimensions = imageMedia.dimensions?.cgSize + } + + let imageButtonView: HighlightTrackingButton + let imageNode: TransformImageNode + if let current = self.imageNode, let currentButton = self.imageButtonView { + imageNode = current + imageButtonView = currentButton + } else { + imageNode = TransformImageNode() + imageNode.displaysAsynchronously = false + imageNode.isUserInteractionEnabled = false + self.imageNode = imageNode + + imageButtonView = HighlightTrackingButton() + imageButtonView.isEnabled = false + self.imageButtonView = imageButtonView + + self.containerButton.addSubview(imageNode.view) + self.addSubview(imageButtonView) + + var imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + if let imageReference = mediaReference.concrete(TelegramMediaImage.self) { + imageSignal = mediaGridMessagePhoto(account: component.context.account, userLocation: .peer(peer.id), photoReference: imageReference) + } else if let fileReference = mediaReference.concrete(TelegramMediaFile.self) { + imageSignal = mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .peer(peer.id), videoReference: fileReference, autoFetchFullSizeThumbnail: true) + } + if let imageSignal { + imageNode.setSignal(imageSignal) + } + } + + if let dimensions { + let makeImageLayout = imageNode.asyncLayout() + let applyImageLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 5.0), imageSize: dimensions.aspectFilled(contentImageSize), boundingSize: contentImageSize, intrinsicInsets: UIEdgeInsets())) + applyImageLayout() + + let imageFrame = CGRect(origin: CGPoint(x: availableSize.width - contentImageSize.width - 10.0 - contextInset, y: floorToScreenPixels((height - contentImageSize.height) / 2.0)), size: contentImageSize) + imageNode.frame = imageFrame + transition.setFrame(view: imageButtonView, frame: imageFrame) + } + } else { + self.imageNode?.removeFromSupernode() + self.imageNode = nil + self.imageButtonView?.removeFromSuperview() + self.imageButtonView = nil + } + if themeUpdated { self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index fca016a27d6..5acf908a083 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -112,6 +112,10 @@ public final class StoryContentContextImpl: StoryContentContext { } } } + } else if case let .channelMessage(_, messageId) = mediaArea { + if let peer = transaction.getPeer(messageId.peerId) { + peers[peer.id] = peer + } } } } @@ -216,11 +220,23 @@ public final class StoryContentContextImpl: StoryContentContext { guard let media = item.media else { return nil } + + var forwardInfo = item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) } + if forwardInfo == nil { + for mediaArea in item.mediaAreas { + if case let .channelMessage(_, messageId) = mediaArea, let peer = peers[messageId.peerId] { + forwardInfo = .known(peer: EnginePeer(peer), storyId: 0, isModified: false) + break + } + } + } + return EngineStoryItem( id: item.id, timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -248,7 +264,7 @@ public final class StoryContentContextImpl: StoryContentContext { isEdited: item.isEdited, isMy: item.isMy, myReaction: item.myReaction, - forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) } + forwardInfo: forwardInfo ) } var totalCount = peerStoryItemsView.items.count @@ -267,6 +283,7 @@ public final class StoryContentContextImpl: StoryContentContext { timestamp: item.timestamp, expirationTimestamp: Int32.max, media: EngineMedia(item.media), + alternativeMedia: nil, mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -929,6 +946,7 @@ public final class StoryContentContextImpl: StoryContentContext { peer: peerReference, storyId: item.id, media: item.media, + alternativeMedia: item.alternativeMedia, reactions: reactions, priority: .top(position: nextPriority) ) @@ -940,7 +958,7 @@ public final class StoryContentContextImpl: StoryContentContext { for (id, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) { validIds.append(id) if self.preloadStoryResourceDisposables[id] == nil { - self.preloadStoryResourceDisposables[id] = preloadStoryMedia(context: context, peer: info.peer, storyId: info.storyId, media: info.media, reactions: info.reactions).startStrict() + self.preloadStoryResourceDisposables[id] = preloadStoryMedia(context: context, info: info).startStrict() } } @@ -1131,6 +1149,10 @@ public final class SingleStoryContentContextImpl: StoryContentContext { } } } + } else if case let .channelMessage(_, messageId) = mediaArea { + if let peer = transaction.getPeer(messageId.peerId) { + peers[peer.id] = peer + } } } } @@ -1186,11 +1208,22 @@ public final class SingleStoryContentContextImpl: StoryContentContext { } if let item, case let .item(itemValue) = item, let media = itemValue.media { + var forwardInfo = itemValue.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) } + if forwardInfo == nil { + for mediaArea in itemValue.mediaAreas { + if case let .channelMessage(_, messageId) = mediaArea, let peer = peers[messageId.peerId] { + forwardInfo = .known(peer: EnginePeer(peer), storyId: 0, isModified: false) + break + } + } + } + let mappedItem = EngineStoryItem( id: itemValue.id, timestamp: itemValue.timestamp, expirationTimestamp: itemValue.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: itemValue.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: itemValue.mediaAreas, text: itemValue.text, entities: itemValue.entities, @@ -1218,7 +1251,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isEdited: itemValue.isEdited, isMy: itemValue.isMy, myReaction: itemValue.myReaction, - forwardInfo: itemValue.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) } + forwardInfo: forwardInfo ) let mainItem = StoryContentItem( @@ -1501,6 +1534,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { peer: peerReference, storyId: item.id, media: item.media, + alternativeMedia: item.alternativeMedia, reactions: reactions, priority: .top(position: nextPriority) ) @@ -1514,7 +1548,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { if let mediaId = info.media.id { validIds.append(mediaId) if self.preloadStoryResourceDisposables[mediaId] == nil { - self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: context, peer: info.peer, storyId: info.storyId, media: info.media, reactions: info.reactions).startStrict() + self.preloadStoryResourceDisposables[mediaId] = preloadStoryMedia(context: context, info: info).startStrict() } } } @@ -1600,13 +1634,20 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } } -public func preloadStoryMedia(context: AccountContext, peer: PeerReference, storyId: Int32, media: EngineMedia, reactions: [MessageReaction.Reaction]) -> Signal { +public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) -> Signal { var signals: [Signal] = [] - switch media { + let selectedMedia: EngineMedia + if context.sharedContext.immediateExperimentalUISettings.alternativeStoryMedia, let alternativeMedia = info.alternativeMedia { + selectedMedia = alternativeMedia + } else { + selectedMedia = info.media + } + + switch selectedMedia { case let .image(image): if let representation = largestImageRepresentation(image.representations) { - signals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyId, media: media._asMedia()), resource: representation.resource), range: nil) + signals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(info.peer.id), userContentType: .story, reference: .media(media: .story(peer: info.peer, id: info.storyId, media: selectedMedia._asMedia()), resource: representation.resource), range: nil) |> ignoreValues |> `catch` { _ -> Signal in return .complete() @@ -1623,7 +1664,7 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor } } - signals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyId, media: media._asMedia()), resource: file.resource), range: fetchRange) + signals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(info.peer.id), userContentType: .story, reference: .media(media: .story(peer: info.peer, id: info.storyId, media: selectedMedia._asMedia()), resource: file.resource), range: fetchRange) |> ignoreValues |> `catch` { _ -> Signal in return .complete() @@ -1636,7 +1677,7 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor var builtinReactions: [String] = [] var customReactions: [Int64] = [] - for reaction in reactions { + for reaction in info.reactions { switch reaction { case let .builtin(value): if !builtinReactions.contains(value) { @@ -1687,8 +1728,6 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor return Void() } - //let fileFetchPriorityDisposable = context.engine.resources.pushPriorityDownload(resourceId: file.resource.id.stringRepresentation, priority: 1) - let statusDisposable = statusSignal.start(completed: { subscriber.putCompletion() }) @@ -1697,7 +1736,6 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor return ActionDisposable { statusDisposable.dispose() loadDisposable.dispose() - //fileFetchPriorityDisposable.dispose() } } }) @@ -1742,12 +1780,9 @@ public func preloadStoryMedia(context: AccountContext, peer: PeerReference, stor }) let loadDisposable = loadSignal.start() - //let fileFetchPriorityDisposable = context.engine.resources.pushPriorityDownload(resourceId: file.resource.id.stringRepresentation, priority: 1) - return ActionDisposable { statusDisposable.dispose() loadDisposable.dispose() - //fileFetchPriorityDisposable.dispose() } } }) @@ -2009,6 +2044,7 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin timestamp: item.timestamp, expirationTimestamp: item.expirationTimestamp, media: EngineMedia(media), + alternativeMedia: item.alternativeMedia.flatMap(EngineMedia.init), mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, @@ -2042,3 +2078,723 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin return nil } } + + +public final class RepostStoriesContentContextImpl: StoryContentContext { + private final class PeerContext { + private let context: AccountContext + let peerId: EnginePeer.Id + + private(set) var sliceValue: StoryContentContextState.FocusedSlice? + fileprivate var nextItems: [EngineStoryItem] = [] + + let updated = Promise() + + private(set) var isReady: Bool = false + + private var disposable: Disposable? + private var loadDisposable: Disposable? + + private let currentFocusedIdUpdatedPromise = Promise() + private var storedFocusedId: Int32? + private var currentMappedItems: [EngineStoryItem]? + var currentFocusedId: Int32? { + didSet { + if self.currentFocusedId != self.storedFocusedId { + self.storedFocusedId = self.currentFocusedId + self.currentFocusedIdUpdatedPromise.set(.single(Void())) + } + } + } + + private var currentForwardInfoStories: [StoryId: Promise] = [:] + + init( + context: AccountContext, + originalPeerId: EnginePeer.Id, + originalStory: EngineStoryItem, + peerId: EnginePeer.Id, + focusedId initialFocusedId: Int32?, + items: [EngineStoryItem] + ) { + self.context = context + self.peerId = peerId + + self.currentFocusedId = initialFocusedId + self.currentFocusedIdUpdatedPromise.set(.single(Void())) + + let originalStoryId = StoryId(peerId: originalPeerId, id: originalStory.id) + + let inputKeys: [PostboxViewKey] = [ + PostboxViewKey.basicPeer(peerId), + PostboxViewKey.cachedPeerData(peerId: peerId), + PostboxViewKey.peerPresences(peerIds: Set([peerId])) + ] + self.disposable = (combineLatest(queue: .mainQueue(), + self.currentFocusedIdUpdatedPromise.get(), + context.account.postbox.combinedView( + keys: inputKeys + ), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()) + ) + |> mapToSignal { _, views, globalNotificationSettings -> Signal<(CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings, [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]), NoError> in + return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings, [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]) in + var peers: [PeerId: Peer] = [:] + var forwardInfoStories: [StoryId: EngineStoryItem?] = [:] + var allEntityFiles: [MediaId: TelegramMediaFile] = [:] + + for item in items { + if let forwardInfo = item.forwardInfo, case let .known(peer, id, _) = forwardInfo { + let storyId = StoryId(peerId: peer.id, id: id) + if storyId == originalStoryId { + forwardInfoStories[storyId] = originalStory + } else { + forwardInfoStories.updateValue(nil, forKey: storyId) + } + } + for entity in item.entities { + if case let .CustomEmoji(_, fileId) = entity.type { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if allEntityFiles[mediaId] == nil { + if let file = transaction.getMedia(mediaId) as? TelegramMediaFile { + allEntityFiles[file.fileId] = file + } + } + } + } + for mediaArea in item.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea { + if case let .custom(fileId) = reaction { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if allEntityFiles[mediaId] == nil { + if let file = transaction.getMedia(mediaId) as? TelegramMediaFile { + allEntityFiles[file.fileId] = file + } + } + } + } else if case let .channelMessage(_, messageId) = mediaArea { + if let peer = transaction.getPeer(messageId.peerId) { + peers[peer.id] = peer + } + } + } + } + + return (views, peers, globalNotificationSettings, allEntityFiles, forwardInfoStories) + } + } + |> deliverOnMainQueue).startStrict(next: { [weak self] views, peers, globalNotificationSettings, allEntityFiles, forwardInfoStories in + guard let self else { + return + } + guard let peerView = views.views[PostboxViewKey.basicPeer(peerId)] as? BasicPeerView else { + return + } + guard let peer = peerView.peer.flatMap(EnginePeer.init) else { + return + } + let additionalPeerData: StoryContentContextState.AdditionalPeerData + var peerPresence: PeerPresence? + if let presencesView = views.views[PostboxViewKey.peerPresences(peerIds: Set([peerId]))] as? PeerPresencesView { + peerPresence = presencesView.presences[peerId] + } + for (storyId, story) in forwardInfoStories { + let promise: Promise + var added = false + if let current = self.currentForwardInfoStories[storyId] { + promise = current + } else { + promise = Promise() + self.currentForwardInfoStories[storyId] = promise + added = true + } + if storyId == originalStoryId { + promise.set(.single(originalStory)) + } else if let story { + promise.set(.single(story)) + } else if added { + promise.set(self.context.engine.messages.getStory(peerId: storyId.peerId, id: storyId.id)) + } + } + + if let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView { + if let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData { + var isMuted = false + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings, topSearchPeers: []) + } else { + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: nil, topSearchPeers: []) + } + additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: isMuted, + areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable, + presence: peerPresence.flatMap { EnginePeer.Presence($0) }, + canViewStats: false + ) + } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { + additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: true, + areVoiceMessagesAvailable: true, + presence: peerPresence.flatMap { EnginePeer.Presence($0) }, + canViewStats: cachedChannelData.flags.contains(.canViewStats) + ) + } else { + additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: true, + areVoiceMessagesAvailable: true, + presence: peerPresence.flatMap { EnginePeer.Presence($0) }, + canViewStats: false + ) + } + } + else { + additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: true, + areVoiceMessagesAvailable: true, + presence: peerPresence.flatMap { EnginePeer.Presence($0) }, + canViewStats: false + ) + } + + let mappedItems = items + let totalCount = mappedItems.count + + let currentFocusedId = self.storedFocusedId + + var focusedIndex: Int? + if let currentFocusedId { + focusedIndex = mappedItems.firstIndex(where: { $0.id == currentFocusedId }) + if focusedIndex == nil { + if let currentMappedItems = self.currentMappedItems { + if let previousIndex = currentMappedItems.firstIndex(where: { $0.id == currentFocusedId }) { + if currentMappedItems[previousIndex].isPending { + if let updatedId = context.engine.messages.lookUpPendingStoryIdMapping(peerId: peerId, stableId: currentFocusedId) { + if let index = mappedItems.firstIndex(where: { $0.id == updatedId }) { + focusedIndex = index + } + } + } + + if focusedIndex == nil && previousIndex != 0 { + for index in (0 ..< previousIndex).reversed() { + if let value = mappedItems.firstIndex(where: { $0.id == currentMappedItems[index].id }) { + focusedIndex = value + break + } + } + } + } + } + } + } + if focusedIndex == nil { + if !mappedItems.isEmpty { + focusedIndex = 0 + } + } + + self.currentMappedItems = mappedItems + + if let focusedIndex { + self.storedFocusedId = mappedItems[focusedIndex].id + + var previousItemId: Int32? + var nextItemId: Int32? + + if focusedIndex != 0 { + previousItemId = mappedItems[focusedIndex - 1].id + } + if focusedIndex != mappedItems.count - 1 { + nextItemId = mappedItems[focusedIndex + 1].id + } + + let mappedFocusedIndex = mappedItems.firstIndex(where: { $0.id == mappedItems[focusedIndex].id }) + + do { + let mappedItem = mappedItems[focusedIndex] + + var nextItems: [EngineStoryItem] = [] + for i in (focusedIndex + 1) ..< min(focusedIndex + 4, mappedItems.count) { + do { + let item = mappedItems[i] + nextItems.append(item) + } + } + + let allItems = mappedItems.map { item in + return StoryContentItem( + position: nil, + dayCounters: nil, + peerId: peer.id, + storyItem: item, + entityFiles: extractItemEntityFiles(item: item, allEntityFiles: allEntityFiles) + ) + } + + self.nextItems = nextItems + self.sliceValue = StoryContentContextState.FocusedSlice( + peer: peer, + additionalPeerData: additionalPeerData, + item: StoryContentItem( + position: mappedFocusedIndex ?? focusedIndex, + dayCounters: nil, + peerId: peer.id, + storyItem: mappedItem, + entityFiles: extractItemEntityFiles(item: mappedItem, allEntityFiles: allEntityFiles) + ), + totalCount: totalCount, + previousItemId: previousItemId, + nextItemId: nextItemId, + allItems: allItems, + forwardInfoStories: self.currentForwardInfoStories + ) + self.isReady = true + self.updated.set(.single(Void())) + } + } else { + self.isReady = true + self.updated.set(.single(Void())) + } + }) + } + + deinit { + self.disposable?.dispose() + self.loadDisposable?.dispose() + } + } + + private final class StateContext { + let centralPeerContext: PeerContext + let previousPeerContext: PeerContext? + let nextPeerContext: PeerContext? + + let updated = Promise() + + var isReady: Bool { + if !self.centralPeerContext.isReady { + return false + } + return true + } + + private var centralDisposable: Disposable? + private var previousDisposable: Disposable? + private var nextDisposable: Disposable? + + init( + centralPeerContext: PeerContext, + previousPeerContext: PeerContext?, + nextPeerContext: PeerContext? + ) { + self.centralPeerContext = centralPeerContext + self.previousPeerContext = previousPeerContext + self.nextPeerContext = nextPeerContext + + self.centralDisposable = (centralPeerContext.updated.get() + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + self.updated.set(.single(Void())) + }) + + if let previousPeerContext { + self.previousDisposable = (previousPeerContext.updated.get() + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + self.updated.set(.single(Void())) + }) + } + + if let nextPeerContext { + self.nextDisposable = (nextPeerContext.updated.get() + |> deliverOnMainQueue).startStrict(next: { [weak self] _ in + guard let self else { + return + } + self.updated.set(.single(Void())) + }) + } + } + + deinit { + self.centralDisposable?.dispose() + self.previousDisposable?.dispose() + self.nextDisposable?.dispose() + } + + func findPeerContext(id: EnginePeer.Id) -> PeerContext? { + if self.centralPeerContext.sliceValue?.peer.id == id { + return self.centralPeerContext + } + if let previousPeerContext = self.previousPeerContext, previousPeerContext.sliceValue?.peer.id == id { + return previousPeerContext + } + if let nextPeerContext = self.nextPeerContext, nextPeerContext.sliceValue?.peer.id == id { + return nextPeerContext + } + return nil + } + } + + private final class PeerStoryItem { + let peer: EnginePeer + let story: EngineStoryItem + + init(peer: EnginePeer, story: EngineStoryItem) { + self.peer = peer + self.story = story + } + } + + private let context: AccountContext + private let originalPeerId: EnginePeer.Id + private let originalStory: EngineStoryItem + private let viewListContext: EngineStoryViewListContext + private let readGlobally: Bool + + public private(set) var stateValue: StoryContentContextState? + public var state: Signal { + return self.statePromise.get() + } + private let statePromise = Promise() + + private let updatedPromise = Promise() + public var updated: Signal { + return self.updatedPromise.get() + } + + private var focusedItem: (peerId: EnginePeer.Id, storyId: Int32?)? + + private var currentState: StateContext? + private var currentStateUpdatedDisposable: Disposable? + + private var pendingState: StateContext? + private var pendingStateReadyDisposable: Disposable? + + private var storyItems: [PeerStoryItem]? + private var storySubscriptionsDisposable: Disposable? + + private var requestedStoryKeys = Set() + private var requestStoryDisposables = DisposableSet() + + private var preloadStoryResourceDisposables: [MediaId: Disposable] = [:] + private var pollStoryMetadataDisposables: [StoryId: Disposable] = [:] + + public init( + context: AccountContext, + originalPeerId: EnginePeer.Id, + originalStory: EngineStoryItem, + focusedStoryId: StoryId, + viewListContext: EngineStoryViewListContext, + readGlobally: Bool + ) { + self.context = context + self.originalPeerId = originalPeerId + self.originalStory = originalStory + self.focusedItem = (focusedStoryId.peerId, focusedStoryId.id) + self.viewListContext = viewListContext + self.readGlobally = readGlobally + + self.storySubscriptionsDisposable = (viewListContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] viewListState in + guard let self else { + return + } + + let storyItems = viewListState.items.compactMap { item in + if let story = item.story { + return PeerStoryItem(peer: item.peer, story: story) + } + return nil + } + + var centralIndex: Int? + if let (focusedPeerId, _) = self.focusedItem { + if let index = storyItems.firstIndex(where: { $0.peer.id == focusedPeerId }) { + centralIndex = index + } + } + if centralIndex == nil && !storyItems.isEmpty { + centralIndex = 0 + } + + self.storyItems = storyItems + self.updatePeerContexts() + }) + } + + deinit { + self.storySubscriptionsDisposable?.dispose() + self.requestStoryDisposables.dispose() + for (_, disposable) in self.preloadStoryResourceDisposables { + disposable.dispose() + } + for (_, disposable) in self.pollStoryMetadataDisposables { + disposable.dispose() + } + self.storySubscriptionsDisposable?.dispose() + self.currentStateUpdatedDisposable?.dispose() + self.pendingStateReadyDisposable?.dispose() + } + + private func updatePeerContexts() { + if let currentState = self.currentState, let storyItems = self.storyItems, !storyItems.contains(where: { $0.peer.id == currentState.centralPeerContext.peerId }) { + self.currentState = nil + } + + if self.currentState == nil { + self.switchToFocusedPeerId() + } + } + + private func switchToFocusedPeerId() { + if let currentStoryItems = self.storyItems { + if self.pendingState == nil { + var centralIndex: Int? + if let (focusedPeerId, _) = self.focusedItem { + if let index = currentStoryItems.firstIndex(where: { $0.peer.id == focusedPeerId }) { + centralIndex = index + } + } + if centralIndex == nil { + if !currentStoryItems.isEmpty { + centralIndex = 0 + } + } + + if let centralIndex { + let centralPeerContext: PeerContext + if let currentState = self.currentState, let existingContext = currentState.findPeerContext(id: currentStoryItems[centralIndex].peer.id) { + centralPeerContext = existingContext + } else { + centralPeerContext = PeerContext(context: self.context, originalPeerId: self.originalPeerId, originalStory: self.originalStory, peerId: currentStoryItems[centralIndex].peer.id, focusedId: nil, items: [currentStoryItems[centralIndex].story]) + } + + var previousPeerContext: PeerContext? + if centralIndex != 0 { + if let currentState = self.currentState, let existingContext = currentState.findPeerContext(id: currentStoryItems[centralIndex - 1].peer.id) { + previousPeerContext = existingContext + } else { + previousPeerContext = PeerContext(context: self.context, originalPeerId: self.originalPeerId, originalStory: self.originalStory, peerId: currentStoryItems[centralIndex - 1].peer.id, focusedId: nil, items: [currentStoryItems[centralIndex - 1].story]) + } + } + + var nextPeerContext: PeerContext? + if centralIndex != currentStoryItems.count - 1 { + if let currentState = self.currentState, let existingContext = currentState.findPeerContext(id: currentStoryItems[centralIndex + 1].peer.id) { + nextPeerContext = existingContext + } else { + nextPeerContext = PeerContext(context: self.context, originalPeerId: self.originalPeerId, originalStory: self.originalStory, peerId: currentStoryItems[centralIndex + 1].peer.id, focusedId: nil, items: [currentStoryItems[centralIndex + 1].story]) + } + } + + let pendingState = StateContext( + centralPeerContext: centralPeerContext, + previousPeerContext: previousPeerContext, + nextPeerContext: nextPeerContext + ) + self.pendingState = pendingState + self.pendingStateReadyDisposable = (pendingState.updated.get() + |> deliverOnMainQueue).startStrict(next: { [weak self, weak pendingState] _ in + guard let self, let pendingState, self.pendingState === pendingState, pendingState.isReady else { + return + } + self.pendingState = nil + self.pendingStateReadyDisposable?.dispose() + self.pendingStateReadyDisposable = nil + + self.currentState = pendingState + + self.updateState() + + self.currentStateUpdatedDisposable?.dispose() + self.currentStateUpdatedDisposable = (pendingState.updated.get() + |> deliverOnMainQueue).startStrict(next: { [weak self, weak pendingState] _ in + guard let self, let pendingState, self.currentState === pendingState else { + return + } + self.updateState() + }) + }) + } + } + } else { + self.updateState() + } + } + + private func updateState() { + guard let currentState = self.currentState else { + return + } + let stateValue = StoryContentContextState( + slice: currentState.centralPeerContext.sliceValue, + previousSlice: currentState.previousPeerContext?.sliceValue, + nextSlice: currentState.nextPeerContext?.sliceValue + ) + self.stateValue = stateValue + self.statePromise.set(.single(stateValue)) + + self.updatedPromise.set(.single(Void())) + + var possibleItems: [(EnginePeer, EngineStoryItem)] = [] + var pollItems: [StoryKey] = [] + if let slice = currentState.centralPeerContext.sliceValue { + var shouldPollItem = false + if slice.peer.id == self.context.account.peerId { + shouldPollItem = true + } else if case .channel = slice.peer { + shouldPollItem = true + } + if shouldPollItem { + pollItems.append(StoryKey(peerId: slice.peer.id, id: slice.item.storyItem.id)) + } + + for item in currentState.centralPeerContext.nextItems { + possibleItems.append((slice.peer, item)) + + var shouldPollNextItem = false + if slice.peer.id == self.context.account.peerId { + shouldPollNextItem = true + } else if case .channel = slice.peer { + shouldPollNextItem = true + } + if shouldPollNextItem { + pollItems.append(StoryKey(peerId: slice.peer.id, id: item.id)) + } + } + } + if let nextPeerContext = currentState.nextPeerContext, let slice = nextPeerContext.sliceValue { + possibleItems.append((slice.peer, slice.item.storyItem)) + for item in nextPeerContext.nextItems { + possibleItems.append((slice.peer, item)) + } + } + + var nextPriority = 0 + var resultResources: [EngineMedia.Id: StoryPreloadInfo] = [:] + for i in 0 ..< min(possibleItems.count, 3) { + let peer = possibleItems[i].0 + let item = possibleItems[i].1 + if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.media.id { + var reactions: [MessageReaction.Reaction] = [] + for mediaArea in item.mediaAreas { + if case let .reaction(_, reaction, _) = mediaArea { + if !reactions.contains(reaction) { + reactions.append(reaction) + } + } + } + resultResources[mediaId] = StoryPreloadInfo( + peer: peerReference, + storyId: item.id, + media: item.media, + alternativeMedia: item.alternativeMedia, + reactions: reactions, + priority: .top(position: nextPriority) + ) + nextPriority += 1 + } + } + + var validIds: [EngineMedia.Id] = [] + for (id, info) in resultResources.sorted(by: { $0.value.priority < $1.value.priority }) { + validIds.append(id) + if self.preloadStoryResourceDisposables[id] == nil { + self.preloadStoryResourceDisposables[id] = preloadStoryMedia(context: context, info: info).startStrict() + } + } + + var removeIds: [EngineMedia.Id] = [] + for (id, disposable) in self.preloadStoryResourceDisposables { + if !validIds.contains(id) { + removeIds.append(id) + disposable.dispose() + } + } + for id in removeIds { + self.preloadStoryResourceDisposables.removeValue(forKey: id) + } + + var pollIdByPeerId: [EnginePeer.Id: [Int32]] = [:] + for storyKey in pollItems.prefix(3) { + if pollIdByPeerId[storyKey.peerId] == nil { + pollIdByPeerId[storyKey.peerId] = [storyKey.id] + } else { + pollIdByPeerId[storyKey.peerId]?.append(storyKey.id) + } + } + for (peerId, ids) in pollIdByPeerId { + for id in ids { + if self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] == nil { + self.pollStoryMetadataDisposables[StoryId(peerId: peerId, id: id)] = self.context.engine.messages.refreshStoryViews(peerId: peerId, ids: ids).startStrict() + } + } + } + } + + public func resetSideStates() { + guard let currentState = self.currentState else { + return + } + if let previousPeerContext = currentState.previousPeerContext { + previousPeerContext.currentFocusedId = nil + } + if let nextPeerContext = currentState.nextPeerContext { + nextPeerContext.currentFocusedId = nil + } + } + + public func navigate(navigation: StoryContentContextNavigation) { + guard let currentState = self.currentState else { + return + } + + switch navigation { + case let .peer(direction): + switch direction { + case .previous: + if let previousPeerContext = currentState.previousPeerContext, let previousSlice = previousPeerContext.sliceValue { + self.pendingStateReadyDisposable?.dispose() + self.pendingState = nil + self.focusedItem = (previousSlice.peer.id, nil) + self.switchToFocusedPeerId() + } + case .next: + if let nextPeerContext = currentState.nextPeerContext, let nextSlice = nextPeerContext.sliceValue { + self.pendingStateReadyDisposable?.dispose() + self.pendingState = nil + self.focusedItem = (nextSlice.peer.id, nil) + self.switchToFocusedPeerId() + } + } + case let .item(direction): + if let slice = currentState.centralPeerContext.sliceValue { + switch direction { + case .previous: + if let previousItemId = slice.previousItemId { + currentState.centralPeerContext.currentFocusedId = previousItemId + } + case .next: + if let nextItemId = slice.nextItemId { + currentState.centralPeerContext.currentFocusedId = nextItemId + } + case let .id(id): + if slice.allItems.contains(where: { $0.storyItem.id == id }) { + currentState.centralPeerContext.currentFocusedId = id + } + } + } + } + } + + public func markAsSeen(id: StoryId) { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).startStandalone() + } + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 5ebe263fe95..42c75a2a00b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -68,7 +68,7 @@ final class StoryContentCaptionComponent: Component { let longTapAction: (Action) -> Void let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void let controller: () -> ViewController? - let openStory: (EnginePeer, EngineStoryItem) -> Void + let openStory: (EnginePeer, EngineStoryItem?) -> Void init( externalState: ExternalState, @@ -85,7 +85,7 @@ final class StoryContentCaptionComponent: Component { longTapAction: @escaping (Action) -> Void, textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void, controller: @escaping () -> ViewController?, - openStory: @escaping (EnginePeer, EngineStoryItem) -> Void + openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void ) { self.externalState = externalState self.context = context @@ -692,9 +692,9 @@ final class StoryContentCaptionComponent: Component { } } }) - text = nil + text = "" } else { - text = nil + text = "" } case let .unknown(name, _): authorName = name @@ -727,8 +727,8 @@ final class StoryContentCaptionComponent: Component { effectAlignment: .center, minSize: nil, action: { [weak self] in - if let self, case let .known(peer, _, _) = forwardInfo, let story = self.forwardInfoStory { - self.component?.openStory(peer, story) + if let self, case let .known(peer, _, _) = forwardInfo { + self.component?.openStory(peer, self.forwardInfoStory) } else if let controller = self?.component?.controller() as? StoryContainerScreen { let tooltipController = TooltipController(content: .text(component.strings.Story_ForwardAuthorHiddenTooltip), baseFontSize: 17.0, isBlurred: true, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak controller] in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 979bd22dc15..be2f161580e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -574,16 +574,34 @@ final class StoryItemContentComponent: Component { let peerReference = PeerReference(component.peer._asPeer()) + let selectedMedia: EngineMedia var messageMedia: EngineMedia? - switch component.item.media { - case let .image(image): - messageMedia = .image(image) - case let .file(file): - messageMedia = .file(file) - case .unsupported: - self.contentLoaded = true - default: - break + if component.context.sharedContext.immediateExperimentalUISettings.alternativeStoryMedia, let alternativeMedia = component.item.alternativeMedia { + selectedMedia = alternativeMedia + + switch alternativeMedia { + case let .image(image): + messageMedia = .image(image) + case let .file(file): + messageMedia = .file(file) + case .unsupported: + self.contentLoaded = true + default: + break + } + } else { + selectedMedia = component.item.media + + switch component.item.media { + case let .image(image): + messageMedia = .image(image) + case let .file(file): + messageMedia = .file(file) + case .unsupported: + self.contentLoaded = true + default: + break + } } var reloadMedia = false @@ -678,7 +696,7 @@ final class StoryItemContentComponent: Component { strings: component.strings, peer: component.peer, storyId: component.item.id, - media: component.item.media, + media: messageMedia, size: availableSize, isCaptureProtected: component.item.isForwardingDisabled, attemptSynchronous: synchronousLoad, @@ -720,7 +738,7 @@ final class StoryItemContentComponent: Component { } } - switch component.item.media { + switch selectedMedia { case .image, .file: if let unsupportedText = self.unsupportedText { self.unsupportedText = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 120b8608f9b..ad342faa281 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1,3 +1,7 @@ +// MARK: Nicegram StealthMode +import FeatPremiumUI +import NGData +// import Foundation import UIKit import Display @@ -435,6 +439,7 @@ public final class StoryItemSetContainerComponent: Component { var preparingToDisplayViewList: Bool = false var viewListDisplayState: ViewListDisplayState = .hidden + private var targetViewListDisplayStateIsFull: Bool = false private var viewListMetrics: (minHeight: CGFloat, maxHeight: CGFloat, currentHeight: CGFloat)? @@ -978,7 +983,7 @@ public final class StoryItemSetContainerComponent: Component { return abs(rotatedX) <= area.coordinates.width / 100.0 * referenceSize.width / 2.0 * 1.1 && abs(rotatedY) <= area.coordinates.height / 100.0 * referenceSize.height / 2.0 * 1.1 } - for area in component.slice.item.storyItem.mediaAreas { + for area in component.slice.item.storyItem.mediaAreas.reversed() { if case .reaction = area { continue } @@ -1231,6 +1236,8 @@ public final class StoryItemSetContainerComponent: Component { var displayViewLists = false if component.slice.peer.id == component.context.account.peerId { displayViewLists = true + } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + displayViewLists = true } if displayViewLists { @@ -1704,6 +1711,7 @@ public final class StoryItemSetContainerComponent: Component { context: component.context, theme: component.theme, strings: component.strings, + peer: component.slice.peer, storyItem: item.storyItem, myReaction: item.storyItem.myReaction.flatMap { value -> StoryFooterPanelComponent.MyReaction? in var centerAnimation: TelegramMediaFile? @@ -1730,6 +1738,7 @@ public final class StoryItemSetContainerComponent: Component { return StoryFooterPanelComponent.MyReaction(reaction: value, file: centerAnimation, animationFileId: animationFileId) }, isChannel: isChannel, + canViewChannelStats: component.slice.additionalPeerData.canViewStats, canShare: canShare, externalViews: nil, expandFraction: footerExpandFraction, @@ -1912,6 +1921,8 @@ public final class StoryItemSetContainerComponent: Component { var displayViewLists = false if component.slice.peer.id == component.context.account.peerId { displayViewLists = true + } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + displayViewLists = true } if displayViewLists { @@ -3123,6 +3134,8 @@ public final class StoryItemSetContainerComponent: Component { var displayViewLists = false if component.slice.peer.id == component.context.account.peerId { displayViewLists = true + } else if case let .channel(channel) = component.slice.peer, channel.flags.contains(.isCreator) || component.slice.additionalPeerData.canViewStats { + displayViewLists = true } if displayViewLists, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) { @@ -3153,7 +3166,13 @@ public final class StoryItemSetContainerComponent: Component { for (id, views) in preloadViewListIds { if component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] == nil { - let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.peer.id, id: id, views: views, listMode: .everyone, sortMode: .reactionsFirst) + let defaultSortMode: EngineStoryViewListContext.SortMode + if component.slice.peer.id.isGroupOrChannel { + defaultSortMode = .repostsFirst + } else { + defaultSortMode = .reactionsFirst + } + let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.peer.id, id: id, views: views, listMode: .everyone, sortMode: defaultSortMode) component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] = viewList } } @@ -3320,6 +3339,12 @@ public final class StoryItemSetContainerComponent: Component { } self.navigateToPeer(peer: peer, chat: false) }, + openMessage: { [weak self] peer, messageId in + guard let self else { + return + } + self.navigateToPeer(peer: peer, chat: true, subject: .message(id: .id(messageId), highlight: nil, timecode: nil)) + }, peerContextAction: { [weak self] peer, sourceView, gesture in guard let self, let component = self.component else { return @@ -3497,6 +3522,12 @@ public final class StoryItemSetContainerComponent: Component { } self.openPeerStories(peer: peer, avatarNode: avatarNode) }, + openReposts: { [weak self] peer, id, sourceView in + guard let self else { + return + } + self.openReposts(peer: peer, id: id, sourceView: sourceView) + }, openPremiumIntro: { [weak self] in guard let self else { return @@ -4225,46 +4256,55 @@ public final class StoryItemSetContainerComponent: Component { guard let self, let component = self.component else { return } - let context = component.context - let peerId = component.slice.peer.id - let currentResult: ResolvedUrl = .story(peerId: peerId, id: component.slice.item.storyItem.id) - - self.sendMessageContext.openResolved(view: self, result: .story(peerId: peer.id, id: story.id), completion: { [weak self] in - guard let self, let controller = self.component?.controller() as? StoryContainerScreen else { - return - } - if let nextController = controller.navigationController?.viewControllers.last as? StoryContainerScreen { - nextController.customBackAction = { [weak nextController] in - context.sharedContext.openResolvedUrl( - currentResult, - context: context, - urlContext: .generic, - navigationController: nextController?.navigationController as? NavigationController, - forceExternal: false, - openPeer: { _, _ in - }, - sendFile: nil, - sendSticker: nil, - requestMessageActionUrlAuth: nil, - joinVoiceChat: nil, - present: { _, _ in - }, - dismissInput: { - }, - contentContext: nil, - progress: nil, - completion: { [weak nextController] in - Queue.mainQueue().after(0.5) { - nextController?.dismissWithoutTransitionOut() + if let story { + let context = component.context + let peerId = component.slice.peer.id + let currentResult: ResolvedUrl = .story(peerId: peerId, id: component.slice.item.storyItem.id) + + self.sendMessageContext.openResolved(view: self, result: .story(peerId: peer.id, id: story.id), completion: { [weak self] in + guard let self, let controller = self.component?.controller() as? StoryContainerScreen else { + return + } + if let nextController = controller.navigationController?.viewControllers.last as? StoryContainerScreen { + nextController.customBackAction = { [weak nextController] in + context.sharedContext.openResolvedUrl( + currentResult, + context: context, + urlContext: .generic, + navigationController: nextController?.navigationController as? NavigationController, + forceExternal: false, + openPeer: { _, _ in + }, + sendFile: nil, + sendSticker: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { _, _ in + }, + dismissInput: { + }, + contentContext: nil, + progress: nil, + completion: { [weak nextController] in + Queue.mainQueue().after(0.5) { + nextController?.dismissWithoutTransitionOut() + } } - } - ) + ) + } + } + Queue.mainQueue().after(0.5) { + controller.dismissWithoutTransitionOut() + } + }) + } else { + for mediaArea in component.slice.item.storyItem.mediaAreas { + if case .channelMessage = mediaArea { + self.sendMessageContext.activateMediaArea(view: self, mediaArea: mediaArea, immediate: true) + break } } - Queue.mainQueue().after(0.5) { - controller.dismissWithoutTransitionOut() - } - }) + } } )), environment: {}, @@ -5151,7 +5191,7 @@ public final class StoryItemSetContainerComponent: Component { } } - func openPeerStories(peer: EnginePeer, avatarNode: AvatarNode) { + func openPeerStories(peer: EnginePeer, avatarNode: AvatarNode?) { guard let component = self.component else { return } @@ -5162,6 +5202,85 @@ public final class StoryItemSetContainerComponent: Component { StoryContainerScreen.openPeerStories(context: component.context, peerId: peer.id, parentController: controller, avatarNode: avatarNode) } + func openReposts(peer: EnginePeer, id: Int32, sourceView: UIView) { + guard let component = self.component else { + return + } + guard let controller = component.controller() else { + return + } + + guard let viewList = self.viewLists[component.slice.item.storyItem.id], let viewListView = viewList.view.view as? StoryItemSetViewListComponent.View, let viewListContext = viewListView.currentViewList else { + return + } + + let context = component.context + let storyContent = RepostStoriesContentContextImpl(context: context, originalPeerId: component.slice.peer.id, originalStory: component.slice.item.storyItem, focusedStoryId: StoryId(peerId: peer.id, id: id), viewListContext: viewListContext, readGlobally: false) + let _ = (storyContent.state + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak viewListView, weak sourceView] _ in + guard let controller, let sourceView else { + return + } + let transitionIn = StoryContainerScreen.TransitionIn( + sourceView: sourceView, + sourceRect: sourceView.bounds, + sourceCornerRadius: sourceView.bounds.width * 0.5, + sourceIsAvatar: false + ) + + let storyContainerScreen = StoryContainerScreen( + context: context, + content: storyContent, + transitionIn: transitionIn, + transitionOut: { [weak sourceView, weak viewListView] peerId, storyIdValue in + var destinationView: UIView? + if let view = viewListView?.sourceView(storyId: StoryId(peerId: peerId, id: storyIdValue as? Int32 ?? 0)) { + destinationView = view + } else { + destinationView = sourceView + } + if let destinationView { + return StoryContainerScreen.TransitionOut( + destinationView: destinationView, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak destinationView] in + let parentView = UIView() + if let copyView = destinationView?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), + destinationRect: destinationView.bounds, + destinationCornerRadius: destinationView.bounds.width * 0.5, + destinationIsAvatar: false, + completed: { [weak sourceView] in + guard let sourceView else { + return + } + sourceView.isHidden = false + } + ) + } else { + return nil + } + } + ) + viewListView?.setPreviewedItem(signal: storyContainerScreen.focusedItem) + controller.push(storyContainerScreen) + }) + } + private let updateDisposable = MetaDisposable() func openStoryEditing(repost: Bool = false) { guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { @@ -5492,6 +5611,13 @@ public final class StoryItemSetContainerComponent: Component { } private func presentStealthModeUpgradeScreen() { + // MARK: Nicegram StealthMode + if !isPremium() { + PremiumUITgHelper.routeToPremium() + return + } + // + self.sendMessageContext.presentStealthModeUpgrade(view: self, action: { [weak self] in guard let self else { return @@ -6456,9 +6582,14 @@ public final class StoryItemSetContainerComponent: Component { } if !component.slice.item.storyItem.isForwardingDisabled { + // MARK: Nicegram SaveStories + let hasPremium = "".isEmpty + // + let saveText: String = component.strings.Story_Context_SaveToGallery items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) + // MARK: Nicegram SaveStories, change accountUser.isPremium to hasPremium + return generateTintedImage(image: UIImage(bundleImageName: hasPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -6466,7 +6597,8 @@ public final class StoryItemSetContainerComponent: Component { return } - if accountUser.isPremium { + // MARK: Nicegram SaveStories, change accountUser.isPremium to hasPremium + if hasPremium { self.requestSave() } else { self.presentSaveUpgradeScreen() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 832c3c040a9..fc56f247b9e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2642,7 +2642,7 @@ final class StoryItemSetContainerSendMessage { component.context.sharedContext.openResolvedUrl( result, context: component.context, - urlContext: .chat(peerId: peerId, updatedPresentationData: updatedPresentationData), + urlContext: .chat(peerId: peerId, message: nil, updatedPresentationData: updatedPresentationData), navigationController: navigationController, forceExternal: forceExternal, openPeer: { [weak self, weak view] peerId, navigation in @@ -3264,8 +3264,9 @@ final class StoryItemSetContainerSendMessage { controller.push(sheet) }) } - - func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea) { + + private var selectedMediaArea: MediaArea? + func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, immediate: Bool = false) { guard let component = view.component, let controller = component.controller() else { return } @@ -3273,13 +3274,13 @@ final class StoryItemSetContainerSendMessage { let theme = defaultDarkColorPresentationTheme 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 = component.context + var actions: [ContextMenuAction] = [] switch mediaArea { case let .venue(_, venue): - let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - - let context = component.context - actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewLocation, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { [weak controller, weak view] in + let action = { [weak controller, weak view] in + let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let locationController = LocationViewController( context: context, updatedPresentationData: updatedPresentationData, @@ -3302,11 +3303,69 @@ final class StoryItemSetContainerSendMessage { }) } controller?.push(locationController) + } + if immediate { + action() + return + } + actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewLocation, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { + action() + })) + case let .channelMessage(_, messageId): + let action = { [weak self, weak view, weak controller] in + let _ = ((context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: true)) + |> mapToSignal { result -> Signal in + if case let .result(messages) = result { + return .single(messages.first) + } else { + return .complete() + } + }) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view, weak controller] message in + guard let self, let view else { + return + } + if let message, let peer = message.peers[message.id.peerId] { + self.openResolved(view: view, result: .channelMessage(peer: peer, messageId: message.id, timecode: nil)) + } else { + controller?.present(UndoOverlayController(presentationData: updatedPresentationData.initial, content: .info(title: nil, text: updatedPresentationData.initial.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current) + } + }, error: { [weak self, weak view] error in + guard let self, let view else { + return + } + switch error { + case .privateChannel: + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak view] peer in + guard let self, let view else { + return + } + if let peer { + self.openResolved(view: view, result: .peer(peer._asPeer(), .default)) + } + }) + } + }) + } + if immediate { + action() + return + } else { + if self.selectedMediaArea == mediaArea { + action() + return + } + } + actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewMessage, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { + action() })) case .reaction: return } + self.selectedMediaArea = mediaArea + let referenceSize = view.controlsContainerView.frame.size let size = CGSize(width: 16.0, height: mediaArea.coordinates.height / 100.0 * referenceSize.height * 1.1) var frame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height) @@ -3317,6 +3376,7 @@ final class StoryItemSetContainerSendMessage { menuController.centerHorizontally = true menuController.dismissed = { [weak self, weak view] in if let self, let view { + self.selectedMediaArea = nil Queue.mainQueue().after(0.1) { self.menuController = nil view.updateIsProgressPaused() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index a99e63a9c5f..ddbb3b83ee5 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -69,8 +69,10 @@ final class StoryItemSetViewListComponent: Component { let deleteAction: () -> Void let moreAction: (UIView, ContextGesture?) -> Void let openPeer: (EnginePeer) -> Void + let openMessage: (EnginePeer, EngineMessage.Id) -> Void let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void let openPeerStories: (EnginePeer, AvatarNode) -> Void + let openReposts: (EnginePeer, Int32, UIView) -> Void let openPremiumIntro: () -> Void let setIsSearchActive: (Bool) -> Void let controller: () -> ViewController? @@ -93,8 +95,10 @@ final class StoryItemSetViewListComponent: Component { deleteAction: @escaping () -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void, openPeer: @escaping (EnginePeer) -> Void, + openMessage: @escaping (EnginePeer, EngineMessage.Id) -> Void, peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void, openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void, + openReposts: @escaping (EnginePeer, Int32, UIView) -> Void, openPremiumIntro: @escaping () -> Void, setIsSearchActive: @escaping (Bool) -> Void, controller: @escaping () -> ViewController? @@ -116,8 +120,10 @@ final class StoryItemSetViewListComponent: Component { self.deleteAction = deleteAction self.moreAction = moreAction self.openPeer = openPeer + self.openMessage = openMessage self.peerContextAction = peerContextAction self.openPeerStories = openPeerStories + self.openReposts = openReposts self.openPremiumIntro = openPremiumIntro self.setIsSearchActive = setIsSearchActive self.controller = controller @@ -234,6 +240,17 @@ final class StoryItemSetViewListComponent: Component { case repostsFirst = 0 case reactionsFirst = 1 case recentFirst = 2 + + var sortMode: EngineStoryViewListContext.SortMode { + switch self { + case .repostsFirst: + return .repostsFirst + case .reactionsFirst: + return .reactionsFirst + case .recentFirst: + return .recentFirst + } + } } private struct ContentConfigurationKey: Equatable { @@ -257,7 +274,7 @@ final class StoryItemSetViewListComponent: Component { let measureItem = ComponentView() var placeholderImage: UIImage? - var visibleItems: [EnginePeer.Id: ComponentView] = [:] + var visibleItems: [EngineStoryViewListContext.Item.ItemHash: ComponentView] = [:] var visiblePlaceholderViews: [Int: UIImageView] = [:] var emptyIcon: ComponentView? @@ -276,6 +293,9 @@ final class StoryItemSetViewListComponent: Component { var viewListState: EngineStoryViewListContext.State? var requestedLoadMoreToken: EngineStoryViewListContext.LoadMoreToken? + private var previewedItemDisposable: Disposable? + private var previewedItemId: StoryId? + var eventCycleState: EventCycleState? var totalCount: Int? { @@ -322,6 +342,42 @@ final class StoryItemSetViewListComponent: Component { deinit { self.viewListDisposable?.dispose() + self.previewedItemDisposable?.dispose() + } + + func setPreviewedItem(signal: Signal) { + self.previewedItemDisposable?.dispose() + self.previewedItemDisposable = (signal |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] previewedItemId in + guard let self else { + return + } + self.previewedItemId = previewedItemId + + for (itemId, visibleItem) in self.visibleItems { + if let itemView = visibleItem.view as? PeerListItemComponent.View { + let isPreviewing = itemId.peerId == previewedItemId?.peerId && itemId.storyId == previewedItemId?.id + itemView.updateIsPreviewing(isPreviewing: isPreviewing) + + if isPreviewing { + let itemFrame = itemView.frame.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + if !self.scrollView.bounds.intersects(itemFrame.insetBy(dx: 0.0, dy: 20.0)) { + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: 0.0, dy: -40.0), animated: false) + } + } + } + } + }) + } + + func sourceView(storyId: StoryId) -> UIView? { + for (itemId, visibleItem) in self.visibleItems { + if let itemView = visibleItem.view as? PeerListItemComponent.View { + if itemId.peerId == storyId.peerId && itemId.storyId == storyId.id { + return itemView.imageNode?.view + } + } + } + return nil } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -388,7 +444,7 @@ final class StoryItemSetViewListComponent: Component { synchronousLoad = hint.synchronousLoad } - var validIds: [EnginePeer.Id] = [] + var validIds: [EngineStoryViewListContext.Item.ItemHash] = [] var validPlaceholderIds: [Int] = [] if let range = itemLayout.visibleItems(for: visibleBounds) { for index in range.lowerBound ..< range.upperBound { @@ -432,21 +488,21 @@ final class StoryItemSetViewListComponent: Component { var itemTransition = transition.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: true)) let item = viewListState.items[index] - validIds.append(item.peer.id) + validIds.append(item.uniqueId) let visibleItem: ComponentView - if let current = self.visibleItems[item.peer.id] { + if let current = self.visibleItems[item.uniqueId] { visibleItem = current } else { if !transition.animation.isImmediate { itemTransition = .immediate } visibleItem = ComponentView() - self.visibleItems[item.peer.id] = visibleItem + self.visibleItems[item.uniqueId] = visibleItem } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: item.timestamp, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat( + var dateText = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: item.timestamp, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat( dateFormatString: { value in return PresentationStrings.FormattedString(string: component.strings.Chat_MessageSeenTimestamp_Date(value).string, ranges: []) }, @@ -461,6 +517,26 @@ final class StoryItemSetViewListComponent: Component { } )).string + if let story = item.story, !story.text.isEmpty { + dateText += component.strings.Story_Views_Commented + } + + let subtitleAccessory: PeerListItemComponent.SubtitleAccessory + if let _ = item.story { + subtitleAccessory = .repost + } else if let _ = item.message { + subtitleAccessory = .forward + } else { + subtitleAccessory = .checks + } + + var storyItem: EngineStoryItem? + if let story = item.story { + storyItem = story + } else if let _ = item.message { + storyItem = component.storyItem + } + let _ = visibleItem.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( @@ -473,7 +549,7 @@ final class StoryItemSetViewListComponent: Component { peer: item.peer, storyStats: item.storyStats, subtitle: dateText, - subtitleAccessory: .checks, + subtitleAccessory: subtitleAccessory, presence: nil, reaction: item.reaction.flatMap { reaction -> PeerListItemComponent.Reaction in var animationFileId: Int64? @@ -490,7 +566,9 @@ final class StoryItemSetViewListComponent: Component { } case let .custom(fileId): animationFileId = fileId - animationFile = item.reactionFile + if case let .view(view) = item { + animationFile = view.reactionFile + } } return PeerListItemComponent.Reaction( reaction: reaction, @@ -498,13 +576,21 @@ final class StoryItemSetViewListComponent: Component { animationFileId: animationFileId ) }, + story: storyItem, + message: item.message, selectionState: .none, hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil, - action: { [weak self] peer in + action: { [weak self] peer, messageId, sourceView in guard let self, let component = self.component else { return } - component.openPeer(peer) + if let messageId { + component.openMessage(peer, messageId) + } else if let storyItem, let sourceView { + component.openReposts(peer, storyItem.id, sourceView) + } else { + component.openPeer(peer) + } }, contextAction: { peer, view, gesture in component.peerContextAction(peer, view, gesture) @@ -519,7 +605,7 @@ final class StoryItemSetViewListComponent: Component { environment: {}, containerSize: itemFrame.size ) - if let itemView = visibleItem.view { + if let itemView = visibleItem.view as? PeerListItemComponent.View { var animateIn = false if itemView.superview == nil { animateIn = true @@ -527,6 +613,8 @@ final class StoryItemSetViewListComponent: Component { } itemTransition.setFrame(view: itemView, frame: itemFrame) + itemView.updateIsPreviewing(isPreviewing: self.previewedItemId?.peerId == item.peer.id && self.previewedItemId?.id == item.story?.id) + if animateIn, synchronousLoad { itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -534,7 +622,7 @@ final class StoryItemSetViewListComponent: Component { } } - var removeIds: [EnginePeer.Id] = [] + var removeIds: [EngineStoryViewListContext.Item.ItemHash] = [] for (id, visibleItem) in self.visibleItems { if !validIds.contains(id) { removeIds.append(id) @@ -687,15 +775,6 @@ final class StoryItemSetViewListComponent: Component { case .contacts: mappedListMode = .contacts } - let mappedSortMode: EngineStoryViewListContext.SortMode - switch self.configuration.sortMode { - case .repostsFirst: - mappedSortMode = .repostsFirst - case .reactionsFirst: - mappedSortMode = .reactionsFirst - case .recentFirst: - mappedSortMode = .recentFirst - } var parentSource: EngineStoryViewListContext? if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil { @@ -705,16 +784,22 @@ final class StoryItemSetViewListComponent: Component { parentSource = nil } - self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, searchQuery: query, parentSource: parentSource) + self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, searchQuery: query, parentSource: parentSource) } } } else { - if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: .reactionsFirst) { + let defaultSortMode: SortMode + if component.peerId.isGroupOrChannel { + defaultSortMode = .repostsFirst + } else { + defaultSortMode = .reactionsFirst + } + if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: defaultSortMode) { let viewList: EngineStoryViewListContext if let current = component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] { viewList = current } else { - viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: .reactionsFirst) + viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: defaultSortMode.sortMode) component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] = viewList } self.viewList = viewList @@ -726,16 +811,7 @@ final class StoryItemSetViewListComponent: Component { case .contacts: mappedListMode = .contacts } - let mappedSortMode: EngineStoryViewListContext.SortMode - switch self.configuration.sortMode { - case .repostsFirst: - mappedSortMode = .repostsFirst - case .reactionsFirst: - mappedSortMode = .reactionsFirst - case .recentFirst: - mappedSortMode = .recentFirst - } - self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)]) + self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)]) } } } @@ -846,7 +922,7 @@ final class StoryItemSetViewListComponent: Component { presence: nil, selectionState: .none, hasNext: true, - action: { _ in + action: { _, _, _ in } )), environment: {}, @@ -990,7 +1066,7 @@ final class StoryItemSetViewListComponent: Component { var emptyButtonTransition = transition let emptyButton: ComponentView? - if self.query == nil, !component.hasPremium, let views = component.storyItem.views, views.seenCount != 0 { + if self.query == nil, !component.hasPremium && !component.peerId.isGroupOrChannel, let views = component.storyItem.views, views.seenCount != 0 { if let current = self.emptyButton { emptyButton = current } else { @@ -1038,7 +1114,7 @@ final class StoryItemSetViewListComponent: Component { text = component.strings.Story_ViewList_PremiumUpgradeText } } else { - text = component.strings.Story_Views_NoViews + text = component.peerId.isGroupOrChannel ? component.strings.Story_Views_NoReactions : component.strings.Story_Views_NoViews } } else { if let query = self.query, !query.isEmpty { @@ -1057,7 +1133,7 @@ final class StoryItemSetViewListComponent: Component { text = component.strings.Story_ViewList_PremiumUpgradeText } } else { - text = component.strings.Story_Views_NoViews + text = component.peerId.isGroupOrChannel ? component.strings.Story_Views_NoReactions : component.strings.Story_Views_NoViews } } } @@ -1226,6 +1302,10 @@ final class StoryItemSetViewListComponent: Component { private var sortMode: SortMode = .reactionsFirst private var currentSearchQuery: String = "" + public var currentViewList: EngineStoryViewListContext? { + return self.currentContentView?.viewList + } + override init(frame: CGRect) { self.navigationContainerView = UIView() self.navigationContainerView.clipsToBounds = true @@ -1260,6 +1340,14 @@ final class StoryItemSetViewListComponent: Component { return super.hitTest(point, with: event) } + public func setPreviewedItem(signal: Signal) { + self.currentContentView?.setPreviewedItem(signal: signal) + } + + public func sourceView(storyId: StoryId) -> UIView? { + self.currentContentView?.sourceView(storyId: storyId) + } + func animateIn(transition: Transition) { let offset = self.bounds.height - self.navigationBarBackground.frame.minY Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset)) @@ -1292,43 +1380,45 @@ final class StoryItemSetViewListComponent: Component { var items: [ContextMenuItem] = [] let sortMode = self.sortMode - -// items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReposts, icon: { theme in -// return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/Repost"), color: theme.contextMenu.primaryColor) -// }, additionalLeftIcon: { theme in -// if sortMode != .repostsFirst { -// return nil -// } -// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) -// }, action: { [weak self] _, a in -// a(.default) -// -// guard let self else { -// return -// } -// if self.sortMode != .repostsFirst { -// self.sortMode = .repostsFirst -// self.state?.updated(transition: .immediate) -// } -// }))) - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor) - }, additionalLeftIcon: { theme in - if sortMode != .reactionsFirst { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - if self.sortMode != .reactionsFirst { - self.sortMode = .reactionsFirst - self.state?.updated(transition: .immediate) - } - }))) + if component.peerId.isGroupOrChannel { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReposts, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/Repost"), color: theme.contextMenu.primaryColor) + }, additionalLeftIcon: { theme in + if sortMode != .repostsFirst { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if self.sortMode != .repostsFirst { + self.sortMode = .repostsFirst + self.state?.updated(transition: .immediate) + } + }))) + } else { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor) + }, additionalLeftIcon: { theme in + if sortMode != .reactionsFirst { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if self.sortMode != .reactionsFirst { + self.sortMode = .reactionsFirst + self.state?.updated(transition: .immediate) + } + }))) + } items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortRecent, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Time"), color: theme.contextMenu.primaryColor) }, additionalLeftIcon: { theme in @@ -1351,7 +1441,8 @@ final class StoryItemSetViewListComponent: Component { items.append(.separator) let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: component.peerId.isGroupOrChannel ? component.strings.Story_ViewList_ContextSortChannelInfo : component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) let contextItems = ContextController.Items(content: .list(items)) @@ -1391,6 +1482,12 @@ final class StoryItemSetViewListComponent: Component { var updateSubState = false if self.mainViewList == nil { + if component.peerId.isGroupOrChannel { + self.sortMode = .repostsFirst + } else { + self.sortMode = .reactionsFirst + } + self.mainViewListDisposable?.dispose() self.mainViewListDisposable = nil @@ -1531,7 +1628,9 @@ final class StoryItemSetViewListComponent: Component { } let titleText: String - if let totalCount = currentTotalCount, let currentTotalReactionCount { + if component.peerId.isGroupOrChannel { + titleText = component.strings.Story_ViewList_TitleReactions + } else if let totalCount = currentTotalCount, let currentTotalReactionCount { if totalCount > 0 && totalCount > currentTotalReactionCount { titleText = component.strings.Story_ViewList_ViewerCount(Int32(totalCount)) } else { @@ -1583,15 +1682,15 @@ final class StoryItemSetViewListComponent: Component { var displaySearchBar = false var displaySortSelector = false - if !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { + if component.peerId == component.context.account.peerId && !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { } else { - if let views = component.storyItem.views, views.hasList { + if let views = component.storyItem.views, views.hasList || component.peerId.isGroupOrChannel { if let totalCount = currentTotalCount { - if totalCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { + if !component.peerId.isGroupOrChannel, totalCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { displayModeSelector = true displaySearchBar = true } - if (views.reactedCount >= 10 && totalCount >= 20) || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { + if (((component.peerId.isGroupOrChannel && views.forwardCount >= 10 ) || (!component.peerId.isGroupOrChannel && views.reactedCount >= 10)) && totalCount >= 20) || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { displaySortSelector = true } } else { diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 1af28ee73c0..1b1b481aa49 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -38,9 +38,11 @@ public final class StoryFooterPanelComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings + public let peer: EnginePeer public let storyItem: EngineStoryItem public let myReaction: MyReaction? public let isChannel: Bool + public let canViewChannelStats: Bool public let canShare: Bool public let externalViews: EngineStoryItem.Views? public let expandFraction: CGFloat @@ -56,9 +58,11 @@ public final class StoryFooterPanelComponent: Component { context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + peer: EnginePeer, storyItem: EngineStoryItem, myReaction: MyReaction?, isChannel: Bool, + canViewChannelStats: Bool, canShare: Bool, externalViews: EngineStoryItem.Views?, expandFraction: CGFloat, @@ -73,9 +77,11 @@ public final class StoryFooterPanelComponent: Component { self.context = context self.theme = theme self.strings = strings + self.peer = peer self.storyItem = storyItem self.myReaction = myReaction self.isChannel = isChannel + self.canViewChannelStats = canViewChannelStats self.canShare = canShare self.externalViews = externalViews self.expandViewStats = expandViewStats @@ -98,6 +104,9 @@ public final class StoryFooterPanelComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.storyItem != rhs.storyItem { return false } @@ -107,6 +116,9 @@ public final class StoryFooterPanelComponent: Component { if lhs.isChannel != rhs.isChannel { return false } + if lhs.canViewChannelStats != rhs.canViewChannelStats { + return false + } if lhs.externalViews != rhs.externalViews { return false } @@ -186,6 +198,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.alpha = 0.7 self.viewStatsCountText.alpha = 0.7 self.viewStatsLabelText.view?.alpha = 0.7 + self.viewsIconView.alpha = 0.7 self.reactionStatsIcon?.alpha = 0.7 self.reactionStatsText?.alpha = 0.7 self.repostStatsIcon?.alpha = 0.7 @@ -194,6 +207,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.alpha = 1.0 self.viewStatsCountText.alpha = 1.0 self.viewStatsLabelText.view?.alpha = 1.0 + self.viewsIconView.alpha = 1.0 self.reactionStatsIcon?.alpha = 1.0 self.reactionStatsText?.alpha = 1.0 self.repostStatsIcon?.alpha = 1.0 @@ -202,6 +216,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsCountText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsLabelText.view?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.viewsIconView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.repostStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) @@ -383,137 +398,431 @@ public final class StoryFooterPanelComponent: Component { } } - self.viewStatsButton.isEnabled = viewCount != 0 && !component.isChannel + var displayViewLists = false + if case let .channel(channel) = component.peer, channel.flags.contains(.isCreator) || component.canViewChannelStats { + displayViewLists = reactionCount != 0 || forwardCount != 0 + } else { + displayViewLists = viewCount != 0 && !component.isChannel + } - var rightContentOffset: CGFloat = availableSize.width - 12.0 + self.viewStatsButton.isEnabled = displayViewLists + + var regularSegments: [AnimatedCountLabelView.Segment] = [] + if viewCount != 0 { + regularSegments.append(.number(viewCount, NSAttributedString(string: countString(Int64(viewCount)), font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) + } + let viewPart: String if component.isChannel { - var likeStatsTransition = transition - var forwardStatsTransition = transition - - if transition.animation.isImmediate, !isFirstTime, let previousComponent, previousComponent.storyItem.id == component.storyItem.id, previousComponent.expandFraction == component.expandFraction { - likeStatsTransition = .easeInOut(duration: 0.2) - forwardStatsTransition = .easeInOut(duration: 0.2) + viewPart = "" + } else if viewCount == 0 { + viewPart = component.strings.Story_Footer_NoViews + } else { + var string = component.strings.Story_Footer_ViewCount(Int32(viewCount)) + if let range = string.range(of: "|") { + if let nextRange = string.range(of: "|", range: range.upperBound ..< string.endIndex) { + string.removeSubrange(string.startIndex ..< nextRange.upperBound) + } + } + viewPart = string + } + + let viewStatsTextLayout = self.viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, reducedLetterSpacing: true, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) + if self.viewStatsCountText.superview == nil { + self.viewStatsCountText.isUserInteractionEnabled = false + self.externalContainerView.addSubview(self.viewStatsCountText) + } + + let viewStatsLabelSize = self.viewStatsLabelText.update( + transition: .immediate, + component: AnyComponent(Text(text: viewPart, font: Font.regular(15.0), color: .white)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + + var reactionsIconSize: CGSize? + var reactionsTextSize: CGSize? + + var repostsIconSize: CGSize? + var repostsTextSize: CGSize? + + if reactionCount != 0 && !component.isChannel { + var reactionsTransition = transition + let reactionStatsIcon: UIImageView + if let current = self.reactionStatsIcon { + reactionStatsIcon = current + } else { + reactionsTransition = reactionsTransition.withAnimation(.none) + reactionStatsIcon = UIImageView() + reactionStatsIcon.image = UIImage(bundleImageName: "Stories/InputLikeOn")?.withRenderingMode(.alwaysTemplate) + + self.reactionStatsIcon = reactionStatsIcon + self.externalContainerView.addSubview(reactionStatsIcon) } - let likeStatsText: AnimatedCountLabelView - if let current = self.likeStatsText { - likeStatsText = current + transition.setTintColor(view: reactionStatsIcon, color: UIColor(rgb: 0xFF3B30).mixedWith(.white, alpha: component.expandFraction)) + + let reactionStatsText: AnimatedCountLabelView + if let current = self.reactionStatsText { + reactionStatsText = current } else { - likeStatsTransition = likeStatsTransition.withAnimation(.none) - likeStatsText = AnimatedCountLabelView(frame: CGRect()) - likeStatsText.isUserInteractionEnabled = false - self.likeStatsText = likeStatsText + reactionStatsText = AnimatedCountLabelView(frame: CGRect()) + reactionStatsText.isUserInteractionEnabled = false + self.reactionStatsText = reactionStatsText + self.externalContainerView.addSubview(reactionStatsText) } - let reactionStatsLayout = likeStatsText.update( + let reactionStatsLayout = reactionStatsText.update( size: CGSize(width: availableSize.width, height: size.height), segments: [ .number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) ], - transition: (isFirstTime || likeStatsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + reducedLetterSpacing: true, + transition: (isFirstTime || reactionsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) ) - var likeStatsFrame = CGRect(origin: CGPoint(x: rightContentOffset - reactionStatsLayout.size.width, y: floor((size.height - reactionStatsLayout.size.height) * 0.5)), size: reactionStatsLayout.size) - likeStatsFrame.origin.y += component.expandFraction * 45.0 + reactionsTextSize = reactionStatsLayout.size - likeStatsTransition.setPosition(view: likeStatsText, position: likeStatsFrame.center) - likeStatsTransition.setBounds(view: likeStatsText, bounds: CGRect(origin: CGPoint(), size: likeStatsFrame.size)) - var likeStatsAlpha: CGFloat = (1.0 - component.expandFraction) - if reactionCount == 0 { - likeStatsAlpha = 0.0 + let imageSize = CGSize(width: 23.0, height: 23.0) + reactionsIconSize = imageSize + } else { + if let reactionStatsIcon = self.reactionStatsIcon { + self.reactionStatsIcon = nil + reactionStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsIcon] _ in + reactionStatsIcon?.removeFromSuperview() + }) } - likeStatsTransition.setAlpha(view: likeStatsText, alpha: likeStatsAlpha) - likeStatsTransition.setScale(view: likeStatsText, scale: reactionCount == 0 ? 0.001 : 1.0) - if reactionCount != 0 { - rightContentOffset -= reactionStatsLayout.size.width + 1.0 + if let reactionStatsText = self.reactionStatsText { + self.reactionStatsText = nil + reactionStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsText] _ in + reactionStatsText?.removeFromSuperview() + }) + } + } + + if forwardCount != 0 && !component.isChannel { + var repostTransition = transition + let repostStatsIcon: UIImageView + if let current = self.repostStatsIcon { + repostStatsIcon = current + } else { + repostTransition = repostTransition.withAnimation(.none) + repostStatsIcon = UIImageView() + repostStatsIcon.image = UIImage(bundleImageName: "Stories/InputRepost")?.withRenderingMode(.alwaysTemplate) + + self.repostStatsIcon = repostStatsIcon + self.externalContainerView.addSubview(repostStatsIcon) } - let likeButton: ComponentView - if let current = self.likeButton { - likeButton = current + transition.setTintColor(view: repostStatsIcon, color: UIColor(rgb: 0x34c759).mixedWith(.white, alpha: component.expandFraction)) + + let repostStatsText: AnimatedCountLabelView + if let current = self.repostStatsText { + repostStatsText = current } else { - likeButton = ComponentView() - self.likeButton = likeButton + repostStatsText = AnimatedCountLabelView(frame: CGRect()) + repostStatsText.isUserInteractionEnabled = false + self.repostStatsText = repostStatsText + self.externalContainerView.addSubview(repostStatsText) } - - let likeButtonSize = likeButton.update( - transition: likeStatsTransition, - component: AnyComponent(MessageInputActionButtonComponent( - mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId), - storyId: component.storyItem.id, - action: { [weak self] _, action, _ in - guard let self, let component = self.component else { - return - } - guard case .up = action else { - return - } - component.likeAction() - }, - longPressAction: nil, - switchMediaInputMode: { - }, - updateMediaCancelFraction: { _ in - }, - lockMediaRecording: { - }, - stopAndPreviewMediaRecording: { - }, - moreAction: { _, _ in }, - context: component.context, - theme: component.theme, - strings: component.strings, - presentController: { _ in }, - audioRecorder: nil, - videoRecordingStatus: nil - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) + + let repostStatsLayout = repostStatsText.update( + size: CGSize(width: availableSize.width, height: size.height), + segments: [ + .number(forwardCount, NSAttributedString(string: "\(forwardCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) + ], + reducedLetterSpacing: true, + transition: (isFirstTime || repostTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) ) - if let likeButtonView = likeButton.view as? MessageInputActionButtonComponent.View { - if likeButtonView.superview == nil { - self.addSubview(likeButtonView) - } - var likeButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - likeButtonSize.width, y: floor((size.height - likeButtonSize.height) * 0.5)), size: likeButtonSize) - likeButtonFrame.origin.y += component.expandFraction * 45.0 - - if let likeButtonTracingOffsetView = self.likeButtonTracingOffsetView { - let difference = CGPoint(x: likeButtonFrame.midX - likeButtonView.layer.position.x, y: likeButtonFrame.midY - likeButtonView.layer.position.y) - if difference != CGPoint() { - likeStatsTransition.setPosition(view: likeButtonTracingOffsetView, position: likeButtonTracingOffsetView.layer.position.offsetBy(dx: difference.x, dy: difference.y)) - } - } - - likeStatsTransition.setPosition(view: likeButtonView, position: likeButtonFrame.center) - likeStatsTransition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size)) - likeStatsTransition.setAlpha(view: likeButtonView, alpha: 1.0 - component.expandFraction) - - rightContentOffset -= likeButtonSize.width + 14.0 - - if likeStatsText.superview == nil { - likeButtonView.button.view.addSubview(likeStatsText) - } - - likeStatsFrame.origin.x -= likeButtonFrame.minX - likeStatsFrame.origin.y -= likeButtonFrame.minY - likeStatsTransition.setPosition(view: likeStatsText, position: likeStatsFrame.center) - likeStatsTransition.setBounds(view: likeStatsText, bounds: CGRect(origin: CGPoint(), size: likeStatsFrame.size)) + repostsTextSize = repostStatsLayout.size + + let imageSize = CGSize(width: 23.0, height: 23.0) + repostsIconSize = imageSize + } else { + if let repostStatsIcon = self.repostStatsIcon { + self.repostStatsIcon = nil + repostStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repostStatsIcon] _ in + repostStatsIcon?.removeFromSuperview() + }) } - if component.canShare { - let forwardStatsText: AnimatedCountLabelView - if let current = self.forwardStatsText { - forwardStatsText = current - } else { - forwardStatsTransition = forwardStatsTransition.withAnimation(.none) - forwardStatsText = AnimatedCountLabelView(frame: CGRect()) - forwardStatsText.isUserInteractionEnabled = false - self.forwardStatsText = forwardStatsText + if let repostStatsText = self.repostStatsText { + self.repostStatsText = nil + repostStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repostStatsText] _ in + repostStatsText?.removeFromSuperview() + }) + } + } + + let viewsReactionsCollapsedSpacing: CGFloat = 6.0 + let viewsReactionsExpandedSpacing: CGFloat = 8.0 + let viewsReactionsSpacing = viewsReactionsCollapsedSpacing.interpolate(to: viewsReactionsExpandedSpacing, amount: component.expandFraction) + + let avatarViewsSpacing: CGFloat = 18.0 + let viewsIconSpacing: CGFloat = 2.0 + + let reactionsIconSpacing: CGFloat = component.isChannel ? 5.0 : 2.0 + + var contentWidth: CGFloat = 0.0 + + contentWidth += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction) + if let image = self.viewsIconView.image { + if component.isChannel { + contentWidth += image.size.width + viewsIconSpacing + } else { + if viewCount != 0 { + contentWidth += (image.size.width + viewsIconSpacing) * component.expandFraction } - - let forwardStatsLayout = forwardStatsText.update( - size: CGSize(width: availableSize.width, height: size.height), + } + } + + if viewCount == 0 { + contentWidth += viewStatsTextLayout.size.width * (1.0 - component.expandFraction) + } else { + contentWidth += viewStatsTextLayout.size.width + } + if !component.isChannel { + contentWidth += viewStatsLabelSize.width * (1.0 - component.expandFraction) + } + + if component.isChannel { + /*if let reactionsIconSize { + contentWidth += viewsReactionsSpacing + contentWidth += reactionsIconSize.width + }*/ + } else { + if let reactionsIconSize, let reactionsTextSize { + contentWidth += viewsReactionsSpacing + contentWidth += reactionsIconSize.width + contentWidth += reactionsIconSpacing + contentWidth += reactionsTextSize.width + } + if let repostsIconSize, let repostsTextSize { + contentWidth += viewsReactionsSpacing + contentWidth += repostsIconSize.width + contentWidth += reactionsIconSpacing + contentWidth += repostsTextSize.width + } + } + + let minContentX: CGFloat = 16.0 + let maxContentX: CGFloat = (availableSize.width - contentWidth) * 0.5 + var contentX: CGFloat = minContentX.interpolate(to: maxContentX, amount: component.expandFraction) + + let avatarsNodeFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) + transition.setPosition(view: self.avatarsView, position: avatarsNodeFrame.center) + transition.setBounds(view: self.avatarsView, bounds: CGRect(origin: CGPoint(), size: avatarsNodeFrame.size)) + transition.setAlpha(view: self.avatarsView, alpha: avatarsAlpha) + transition.setScale(view: self.avatarsView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: component.expandFraction)) + + if let image = self.viewsIconView.image { + let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - image.size.height) * 0.5)), size: image.size) + transition.setPosition(view: self.viewsIconView, position: viewsIconFrame.center) + transition.setBounds(view: self.viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size)) + + if component.isChannel { + transition.setAlpha(view: self.viewsIconView, alpha: 1.0) + transition.setScale(view: self.viewsIconView, scale: 1.0) + } else { + if viewCount == 0 { + transition.setAlpha(view: self.viewsIconView, alpha: 0.0) + } else { + transition.setAlpha(view: self.viewsIconView, alpha: component.expandFraction) + } + transition.setScale(view: self.viewsIconView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: 1.0 - component.expandFraction)) + } + } + + if component.isChannel { + if let image = self.viewsIconView.image { + contentX += image.size.width + viewsIconSpacing + } + } else { + if !avatarsSize.width.isZero { + contentX += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction) + } + if let image = self.viewsIconView.image { + contentX += (image.size.width + viewsIconSpacing) * component.expandFraction + } + } + + transition.setFrame(view: self.viewStatsCountText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - viewStatsTextLayout.size.height) * 0.5)), size: viewStatsTextLayout.size)) + if viewCount == 0 { + contentX += viewStatsTextLayout.size.width * component.expandFraction + transition.setAlpha(view: self.viewStatsCountText, alpha: component.expandFraction) + } else { + contentX += viewStatsTextLayout.size.width + transition.setAlpha(view: self.viewStatsCountText, alpha: 1.0) + } + + let viewStatsLabelTextFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - viewStatsLabelSize.height) * 0.5)), size: viewStatsLabelSize) + if let viewStatsLabelTextView = self.viewStatsLabelText.view { + if viewStatsLabelTextView.superview == nil { + viewStatsLabelTextView.isUserInteractionEnabled = false + viewStatsLabelTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) + self.externalContainerView.addSubview(viewStatsLabelTextView) + } + transition.setPosition(view: viewStatsLabelTextView, position: CGPoint(x: viewStatsLabelTextFrame.minX, y: viewStatsLabelTextFrame.midY)) + transition.setBounds(view: viewStatsLabelTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsLabelTextFrame.size)) + transition.setAlpha(view: viewStatsLabelTextView, alpha: 1.0 - component.expandFraction) + transition.setScale(view: viewStatsLabelTextView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: component.expandFraction)) + } + if !component.isChannel { + contentX += viewStatsLabelSize.width * (1.0 - component.expandFraction) + } + + if let reactionStatsIcon = self.reactionStatsIcon, let reactionsIconSize, let reactionStatsText = self.reactionStatsText, let reactionsTextSize { + contentX += viewsReactionsSpacing + + transition.setFrame(view: reactionStatsIcon, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - reactionsIconSize.height) * 0.5)), size: reactionsIconSize)) + contentX += reactionsIconSize.width + contentX += reactionsIconSpacing + + transition.setFrame(view: reactionStatsText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - reactionsTextSize.height) * 0.5)), size: reactionsTextSize)) + contentX += reactionsTextSize.width + } + + if let repostStatsIcon = self.repostStatsIcon, let repostsIconSize, let repostStatsText = self.repostStatsText, let repostsTextSize { + contentX += viewsReactionsSpacing + + transition.setFrame(view: repostStatsIcon, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - repostsIconSize.height) * 0.5)), size: repostsIconSize)) + contentX += repostsIconSize.width + contentX += reactionsIconSpacing + + transition.setFrame(view: repostStatsText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - repostsTextSize.height) * 0.5)), size: repostsTextSize)) + contentX += repostsTextSize.width + } + + var rightContentOffset: CGFloat = availableSize.width - 12.0 + + if component.isChannel { + var likeStatsTransition = transition + var forwardStatsTransition = transition + + if transition.animation.isImmediate, !isFirstTime, let previousComponent, previousComponent.storyItem.id == component.storyItem.id, previousComponent.expandFraction == component.expandFraction { + likeStatsTransition = .easeInOut(duration: 0.2) + forwardStatsTransition = .easeInOut(duration: 0.2) + } + + let likeStatsText: AnimatedCountLabelView + if let current = self.likeStatsText { + likeStatsText = current + } else { + likeStatsTransition = likeStatsTransition.withAnimation(.none) + likeStatsText = AnimatedCountLabelView(frame: CGRect()) + likeStatsText.isUserInteractionEnabled = false + self.likeStatsText = likeStatsText + } + + let reactionStatsLayout = likeStatsText.update( + size: CGSize(width: availableSize.width, height: size.height), + segments: [ + .number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) + ], + transition: (isFirstTime || likeStatsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) + ) + var likeStatsFrame = CGRect(origin: CGPoint(x: rightContentOffset - reactionStatsLayout.size.width, y: floor((size.height - reactionStatsLayout.size.height) * 0.5)), size: reactionStatsLayout.size) + likeStatsFrame.origin.y += component.expandFraction * 45.0 + //likeStatsFrame.origin.x = (1.0 - component.expandFraction) * likeStatsFrame.origin.x + component.expandFraction * (contentX) + + likeStatsTransition.setPosition(view: likeStatsText, position: likeStatsFrame.center) + likeStatsTransition.setBounds(view: likeStatsText, bounds: CGRect(origin: CGPoint(), size: likeStatsFrame.size)) + var likeStatsAlpha: CGFloat = (1.0 - component.expandFraction) + if reactionCount == 0 { + likeStatsAlpha = 0.0 + } + likeStatsTransition.setAlpha(view: likeStatsText, alpha: likeStatsAlpha) + likeStatsTransition.setScale(view: likeStatsText, scale: reactionCount == 0 ? 0.001 : 1.0) + + if reactionCount != 0 { + rightContentOffset -= reactionStatsLayout.size.width + 1.0 + } + + let likeButton: ComponentView + if let current = self.likeButton { + likeButton = current + } else { + likeButton = ComponentView() + self.likeButton = likeButton + } + + let likeButtonSize = likeButton.update( + transition: likeStatsTransition, + component: AnyComponent(MessageInputActionButtonComponent( + mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId), + storyId: component.storyItem.id, + action: { [weak self] _, action, _ in + guard let self, let component = self.component else { + return + } + guard case .up = action else { + return + } + component.likeAction() + }, + longPressAction: nil, + switchMediaInputMode: { + }, + updateMediaCancelFraction: { _ in + }, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + moreAction: { _, _ in }, + context: component.context, + theme: component.theme, + strings: component.strings, + presentController: { _ in }, + audioRecorder: nil, + videoRecordingStatus: nil + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + if let likeButtonView = likeButton.view as? MessageInputActionButtonComponent.View { + if likeButtonView.superview == nil { + self.addSubview(likeButtonView) + } + var likeButtonFrame = CGRect(origin: CGPoint(x: rightContentOffset - likeButtonSize.width, y: floor((size.height - likeButtonSize.height) * 0.5)), size: likeButtonSize) + likeButtonFrame.origin.y += component.expandFraction * 45.0 + + if let likeButtonTracingOffsetView = self.likeButtonTracingOffsetView { + let difference = CGPoint(x: likeButtonFrame.midX - likeButtonView.layer.position.x, y: likeButtonFrame.midY - likeButtonView.layer.position.y) + if difference != CGPoint() { + likeStatsTransition.setPosition(view: likeButtonTracingOffsetView, position: likeButtonTracingOffsetView.layer.position.offsetBy(dx: difference.x, dy: difference.y)) + } + } + + likeStatsTransition.setPosition(view: likeButtonView, position: likeButtonFrame.center) + likeStatsTransition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size)) + likeStatsTransition.setAlpha(view: likeButtonView, alpha: 1.0 - component.expandFraction) + + rightContentOffset -= likeButtonSize.width + 14.0 + + if likeStatsText.superview == nil { + likeButtonView.button.view.addSubview(likeStatsText) + } + + likeStatsFrame.origin.x -= likeButtonFrame.minX + likeStatsFrame.origin.y -= likeButtonFrame.minY + likeStatsTransition.setPosition(view: likeStatsText, position: likeStatsFrame.center) + likeStatsTransition.setBounds(view: likeStatsText, bounds: CGRect(origin: CGPoint(), size: likeStatsFrame.size)) + } + + if component.canShare { + let forwardStatsText: AnimatedCountLabelView + if let current = self.forwardStatsText { + forwardStatsText = current + } else { + forwardStatsTransition = forwardStatsTransition.withAnimation(.none) + forwardStatsText = AnimatedCountLabelView(frame: CGRect()) + forwardStatsText.isUserInteractionEnabled = false + self.forwardStatsText = forwardStatsText + } + + let forwardStatsLayout = forwardStatsText.update( + size: CGSize(width: availableSize.width, height: size.height), segments: [ .number(forwardCount, NSAttributedString(string: "\(forwardCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) ], @@ -678,292 +987,6 @@ public final class StoryFooterPanelComponent: Component { } } - var regularSegments: [AnimatedCountLabelView.Segment] = [] - if viewCount != 0 { - regularSegments.append(.number(viewCount, NSAttributedString(string: countString(Int64(viewCount)), font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white))) - } - - let viewPart: String - if component.isChannel { - viewPart = "" - } else if viewCount == 0 { - viewPart = component.strings.Story_Footer_NoViews - } else { - var string = component.strings.Story_Footer_ViewCount(Int32(viewCount)) - if let range = string.range(of: "|") { - if let nextRange = string.range(of: "|", range: range.upperBound ..< string.endIndex) { - string.removeSubrange(string.startIndex ..< nextRange.upperBound) - } - } - viewPart = string - } - - let viewStatsTextLayout = self.viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, reducedLetterSpacing: true, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) - if self.viewStatsCountText.superview == nil { - self.viewStatsCountText.isUserInteractionEnabled = false - self.externalContainerView.addSubview(self.viewStatsCountText) - } - - let viewStatsLabelSize = self.viewStatsLabelText.update( - transition: .immediate, - component: AnyComponent(Text(text: viewPart, font: Font.regular(15.0), color: .white)), - environment: {}, - containerSize: CGSize(width: 200.0, height: 100.0) - ) - - var reactionsIconSize: CGSize? - var reactionsTextSize: CGSize? - - var repostsIconSize: CGSize? - var repostsTextSize: CGSize? - - if reactionCount != 0 && !component.isChannel { - var reactionsTransition = transition - let reactionStatsIcon: UIImageView - if let current = self.reactionStatsIcon { - reactionStatsIcon = current - } else { - reactionsTransition = reactionsTransition.withAnimation(.none) - reactionStatsIcon = UIImageView() - reactionStatsIcon.image = UIImage(bundleImageName: "Stories/InputLikeOn")?.withRenderingMode(.alwaysTemplate) - - self.reactionStatsIcon = reactionStatsIcon - self.externalContainerView.addSubview(reactionStatsIcon) - } - - transition.setTintColor(view: reactionStatsIcon, color: UIColor(rgb: 0xFF3B30).mixedWith(.white, alpha: component.expandFraction)) - - let reactionStatsText: AnimatedCountLabelView - if let current = self.reactionStatsText { - reactionStatsText = current - } else { - reactionStatsText = AnimatedCountLabelView(frame: CGRect()) - reactionStatsText.isUserInteractionEnabled = false - self.reactionStatsText = reactionStatsText - self.externalContainerView.addSubview(reactionStatsText) - } - - let reactionStatsLayout = reactionStatsText.update( - size: CGSize(width: availableSize.width, height: size.height), - segments: [ - .number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) - ], - reducedLetterSpacing: true, - transition: (isFirstTime || reactionsTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) - ) - reactionsTextSize = reactionStatsLayout.size - - let imageSize = CGSize(width: 23.0, height: 23.0) - reactionsIconSize = imageSize - } else { - if let reactionStatsIcon = self.reactionStatsIcon { - self.reactionStatsIcon = nil - reactionStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsIcon] _ in - reactionStatsIcon?.removeFromSuperview() - }) - } - - if let reactionStatsText = self.reactionStatsText { - self.reactionStatsText = nil - reactionStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionStatsText] _ in - reactionStatsText?.removeFromSuperview() - }) - } - } - - if forwardCount != 0 && !component.isChannel { - var repostTransition = transition - let repostStatsIcon: UIImageView - if let current = self.repostStatsIcon { - repostStatsIcon = current - } else { - repostTransition = repostTransition.withAnimation(.none) - repostStatsIcon = UIImageView() - repostStatsIcon.image = UIImage(bundleImageName: "Stories/InputRepost")?.withRenderingMode(.alwaysTemplate) - - self.repostStatsIcon = repostStatsIcon - self.externalContainerView.addSubview(repostStatsIcon) - } - - transition.setTintColor(view: repostStatsIcon, color: UIColor(rgb: 0x34c759).mixedWith(.white, alpha: component.expandFraction)) - - let repostStatsText: AnimatedCountLabelView - if let current = self.repostStatsText { - repostStatsText = current - } else { - repostStatsText = AnimatedCountLabelView(frame: CGRect()) - repostStatsText.isUserInteractionEnabled = false - self.repostStatsText = repostStatsText - self.externalContainerView.addSubview(repostStatsText) - } - - let repostStatsLayout = repostStatsText.update( - size: CGSize(width: availableSize.width, height: size.height), - segments: [ - .number(forwardCount, NSAttributedString(string: "\(forwardCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)) - ], - reducedLetterSpacing: true, - transition: (isFirstTime || repostTransition.animation.isImmediate) ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) - ) - repostsTextSize = repostStatsLayout.size - - let imageSize = CGSize(width: 23.0, height: 23.0) - repostsIconSize = imageSize - } else { - if let repostStatsIcon = self.repostStatsIcon { - self.repostStatsIcon = nil - repostStatsIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repostStatsIcon] _ in - repostStatsIcon?.removeFromSuperview() - }) - } - - if let repostStatsText = self.repostStatsText { - self.repostStatsText = nil - repostStatsText.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repostStatsText] _ in - repostStatsText?.removeFromSuperview() - }) - } - } - - let viewsReactionsCollapsedSpacing: CGFloat = 6.0 - let viewsReactionsExpandedSpacing: CGFloat = 8.0 - let viewsReactionsSpacing = viewsReactionsCollapsedSpacing.interpolate(to: viewsReactionsExpandedSpacing, amount: component.expandFraction) - - let avatarViewsSpacing: CGFloat = 18.0 - let viewsIconSpacing: CGFloat = 2.0 - - let reactionsIconSpacing: CGFloat = component.isChannel ? 5.0 : 2.0 - - var contentWidth: CGFloat = 0.0 - - contentWidth += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction) - if let image = self.viewsIconView.image { - if component.isChannel { - contentWidth += image.size.width + viewsIconSpacing - } else { - if viewCount != 0 { - contentWidth += (image.size.width + viewsIconSpacing) * component.expandFraction - } - } - } - - if viewCount == 0 { - contentWidth += viewStatsTextLayout.size.width * (1.0 - component.expandFraction) - } else { - contentWidth += viewStatsTextLayout.size.width - } - if !component.isChannel { - contentWidth += viewStatsLabelSize.width * (1.0 - component.expandFraction) - } - - if component.isChannel { - /*if let reactionsIconSize { - contentWidth += viewsReactionsSpacing - contentWidth += reactionsIconSize.width - }*/ - } else { - if let reactionsIconSize, let reactionsTextSize { - contentWidth += viewsReactionsSpacing - contentWidth += reactionsIconSize.width - contentWidth += reactionsIconSpacing - contentWidth += reactionsTextSize.width - } - if let repostsIconSize, let repostsTextSize { - contentWidth += viewsReactionsSpacing - contentWidth += repostsIconSize.width - contentWidth += reactionsIconSpacing - contentWidth += repostsTextSize.width - } - } - - let minContentX: CGFloat = 16.0 - let maxContentX: CGFloat = (availableSize.width - contentWidth) * 0.5 - var contentX: CGFloat = minContentX.interpolate(to: maxContentX, amount: component.expandFraction) - - let avatarsNodeFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) - transition.setPosition(view: self.avatarsView, position: avatarsNodeFrame.center) - transition.setBounds(view: self.avatarsView, bounds: CGRect(origin: CGPoint(), size: avatarsNodeFrame.size)) - transition.setAlpha(view: self.avatarsView, alpha: avatarsAlpha) - transition.setScale(view: self.avatarsView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: component.expandFraction)) - - if let image = self.viewsIconView.image { - let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - image.size.height) * 0.5)), size: image.size) - transition.setPosition(view: self.viewsIconView, position: viewsIconFrame.center) - transition.setBounds(view: self.viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size)) - - if component.isChannel { - transition.setAlpha(view: self.viewsIconView, alpha: 1.0) - transition.setScale(view: self.viewsIconView, scale: 1.0) - } else { - if viewCount == 0 { - transition.setAlpha(view: self.viewsIconView, alpha: 0.0) - } else { - transition.setAlpha(view: self.viewsIconView, alpha: component.expandFraction) - } - transition.setScale(view: self.viewsIconView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: 1.0 - component.expandFraction)) - } - } - - if component.isChannel { - if let image = self.viewsIconView.image { - contentX += image.size.width + viewsIconSpacing - } - } else { - if !avatarsSize.width.isZero { - contentX += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction) - } - if let image = self.viewsIconView.image { - contentX += (image.size.width + viewsIconSpacing) * component.expandFraction - } - } - - transition.setFrame(view: self.viewStatsCountText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - viewStatsTextLayout.size.height) * 0.5)), size: viewStatsTextLayout.size)) - if viewCount == 0 { - contentX += viewStatsTextLayout.size.width * component.expandFraction - transition.setAlpha(view: self.viewStatsCountText, alpha: component.expandFraction) - } else { - contentX += viewStatsTextLayout.size.width - transition.setAlpha(view: self.viewStatsCountText, alpha: 1.0) - } - - let viewStatsLabelTextFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - viewStatsLabelSize.height) * 0.5)), size: viewStatsLabelSize) - if let viewStatsLabelTextView = self.viewStatsLabelText.view { - if viewStatsLabelTextView.superview == nil { - viewStatsLabelTextView.isUserInteractionEnabled = false - viewStatsLabelTextView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) - self.externalContainerView.addSubview(viewStatsLabelTextView) - } - transition.setPosition(view: viewStatsLabelTextView, position: CGPoint(x: viewStatsLabelTextFrame.minX, y: viewStatsLabelTextFrame.midY)) - transition.setBounds(view: viewStatsLabelTextView, bounds: CGRect(origin: CGPoint(), size: viewStatsLabelTextFrame.size)) - transition.setAlpha(view: viewStatsLabelTextView, alpha: 1.0 - component.expandFraction) - transition.setScale(view: viewStatsLabelTextView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: component.expandFraction)) - } - if !component.isChannel { - contentX += viewStatsLabelSize.width * (1.0 - component.expandFraction) - } - - if let reactionStatsIcon = self.reactionStatsIcon, let reactionsIconSize, let reactionStatsText = self.reactionStatsText, let reactionsTextSize { - contentX += viewsReactionsSpacing - - transition.setFrame(view: reactionStatsIcon, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - reactionsIconSize.height) * 0.5)), size: reactionsIconSize)) - contentX += reactionsIconSize.width - contentX += reactionsIconSpacing - - transition.setFrame(view: reactionStatsText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - reactionsTextSize.height) * 0.5)), size: reactionsTextSize)) - contentX += reactionsTextSize.width - } - - if let repostStatsIcon = self.repostStatsIcon, let repostsIconSize, let repostStatsText = self.repostStatsText, let repostsTextSize { - contentX += viewsReactionsSpacing - - transition.setFrame(view: repostStatsIcon, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - repostsIconSize.height) * 0.5)), size: repostsIconSize)) - contentX += repostsIconSize.width - contentX += reactionsIconSpacing - - transition.setFrame(view: repostStatsText, frame: CGRect(origin: CGPoint(x: contentX, y: floor((size.height - repostsTextSize.height) * 0.5)), size: repostsTextSize)) - contentX += repostsTextSize.width - } - let statsButtonWidth = availableSize.width - 80.0 transition.setFrame(view: self.viewStatsButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: statsButtonWidth, height: baseHeight))) diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift index dbfda1f3727..1b0f27498bf 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -308,6 +308,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false self.placeholderNode.maximumNumberOfLines = 1 + self.placeholderNode.displaysAsynchronously = false self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) self.textFieldScrollNode = ASScrollNode() diff --git a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift index 7903e414e89..7db183dbf5e 100644 --- a/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift +++ b/submodules/TelegramUI/Components/WallpaperPreviewMedia/Sources/WallpaperPreviewMedia.swift @@ -9,6 +9,7 @@ public enum WallpaperPreviewMediaContent: Equatable { case color(UIColor) case gradient([UInt32], Int32?) case themeSettings(TelegramThemeSettings) + case emoticon(String) } public final class WallpaperPreviewMedia: Media { @@ -64,6 +65,8 @@ public extension WallpaperPreviewMedia { self.init(content: .file(file: file.file, colors: file.settings.colors, rotation: file.settings.rotation, intensity: file.settings.intensity, false, false)) case let .image(representations, _): self.init(content: .image(representations: representations)) + case let .emoticon(emoticon): + self.init(content: .emoticon(emoticon)) default: return nil } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/Contents.json new file mode 100644 index 00000000000..d288e5cbca1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "covercolor_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/covercolor_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverColor.imageset/covercolor_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0b24a428b42ff6dc84416588350cbf5f64fbfb46 GIT binary patch literal 10860 zcma)?TW>AJ5ryCPub3Akh{SmM-Yp@da0pS9L?MT~p*%P~22SKla%>`!U!QMP_w>Z(<%YW7E8fBDs?ZY`IKTeju#Z-2im%V(c0pMQS2eR!w;z4$YJ^@rPs z@1K8I0^nPXdV2nFdwqYo`ttT)?{9A3{`QOI%{S-&yLr0&^WxUc{+4b3@ag-uD@$E-PVDLg-CUgiIeK?-aotLG+xc;q)~&@D+E|6D7O>i44SnSh^#pU~HP;eL z(#V=yh+%PM^-LMPuo9!MSy&EXi#B8OC2=gCu4|071RLp`YtFvcrwy!sn^?PX>XKaC z=gC=5gT^gpcEs#j(M*+6*jf>ms;h-hjLVF)ou!mYDE;Zmx%+-A=JjSRUwjU>-_|&G zl?8KZl^bKmf!$m{i{mEFsIcOEKDdTx$0@N~rn+`K)FskHNo^_}xhBTK>> ztX)@^BHJ{~c@8AZ2U)s5?Q)C3>=HI#ot>LcrP(d&+sL{5Gb!)$upU;!F3p#%EFN!* zTPfH)ueH=o`*o|U)o<(~s-K)JTiXDcBlFYji<#Ncp(uqBt5JAvMAUf$wUt-oV^idCIH1HL$sEo5x^Z1MP)yD_;#G{}UK3x#I^24*qp@luY<_`= zZZ1E_iwE}fwtDI-VS0Qf$Um1QxU&d79fy=GTl9ABeX-mVuRw>CULEgpb&cjw&U37CxzJdv`6v+% zk7G-$3Qx)Y?A61{SVGn(>YZg{s3R+uxF`4o`nJ6~pQAjS8|6ki7i>VvgWNpMPTZgb zTE+I{-MChm2dXq3(TGJdBB&=o*c)r_+=kcr2os;Np2sYwoq4clvR5?N5O>^@`7$SR z8=Sbb6-tU^5?u%(jl2LNlwRECgzSh_+@DY5@Z&92R1f1ywnt~UY1IuYpo@qPDvKg+D1*7CBDNf_0FLER|LLm6MPAf0w zq5=w&^s6KZIP{EP(J-)8h?{P)-A~N2iRLAUc|Sl~Xy7G_d8(;Suc57v*@y zNkx)=x%pJd(=VVA(H!kGu4eK~AD5viwuSXj9Yl+*nw77Ne$6Gd=`Q~#c<6kd*+(ic zhRhd~@Jpaw<*%V36r~cd-!Af3$TV}wsK{O8qbm{>fS_ICuV_hJN{>#3?{R&UD#6@H zGbZpo8JEU9kg2t5P65;}GuL=Ngd#&{ba_ zIfs3B%k|~)am>rPA4(#sI)(su18V6VR5lc2cU>Qu$ zDT#>!Li&fQL5N295o0t=N(o{Z2!aLcY|tPM1%Ye<+aWC+6^DcaSrU_(74caVpgl<- zvjP;sNa9ECD^Iip?3 zU7^fb&4BFO)hU+RK-XpzXADbv02w^k36_JM=+dvSkxDkOn!H3l&dxGAA-dL~#XXi$ zA_AeOAYX!^dugh&yX#3~m!E zS<2rAyY#fMV-3d{CNmTy&CX1N6r zSx(&!6k9;|*maTR&@F-pIm_Gl7If1=G!0>lBN64bo=7}p4$u!eFg5W0mgf$&j-V^vqxNC2W_mNG#}<2&e~9-`)$ZE}F1W#P3YQ4cKwv6~btW zxy~!pHB-xVUSWchl;d72VS|3A<>sttm7*p{6%%NA%NsdmFr0B&575TudsfqZtj*7Zvj6-l)+k%cBeLllHX;jmqC~+~3>J2%NSCk;4sC?~d%VI&U zQjNrWVN|HbNpv_8(<&{nO}QO>gI=ewq5DU+#Q#*Ba!`qJ@;w?5gnlrs3Qkbq8VoFq zYKTn4DD{IllR7wbA@!WvP#hQpmPXR(IjS>q7#Q7A>Qg<}h_WU`1$+Y;se2;a5X9B< z`2-;koHrl=PR{~~dAMh77B=9XPY^Mz6_{^=>det#AOz=Z(`Lu26H<14ZQ45wj_bj` zYKRa4cnq#{FLtB+mzE>FBm7NeHPi^QUnL%$R~(plgQE#<6=MWE*DM#Im4;3-kdu%> zx{r|d>|yc^2A4x{yRfC^E1QFJNQE>?M!g9&nI;9q8fvUcjliXP6#AkI(?u+f%p^dS zSTKm3Mq@*16w*?cf=4Sdo!l27TIwjs0wqH*SQtiJN}X>MAD(CAL-9(HOw&UkPmun& zNSIIk2m0cOWPF*J<_1(ms8yaTl&e*X8nE0SAOwf)fM|WF%>fjNH`Q^H^rk-EK-?J7!XVb+f&s`XFo0YYMXsd6eqAp*)Xzr) z8;RiKG86OBB+x$DFgxZ0@j^>WIYQ*hvx4XucA5o*e$>Ap`W)4$*ol4(%{(hSaZeNW z#EE#GQ2@$XS{3<&u&{%vO5jc?37{T~&RHpdgH~0I!U+Yr;5maHRo_J}0oboo2^lLE z<=jdDkyrVeJyAS$*QQYk2a0IbTGlAY4N1y;zYqe7jIu{0iDks!{Rt%M;76va&`dxo zSw0tMu?J=4uIdQqjGfS(U;g7li{d( z;B3GNVF99-d2!zfV#q<8bFu?E?u}>XK`-Kxf+XHp*ilEM&PFIz#(0&Z|e- zs|X&6T7)(Usu7A8OCp=#W*}ZA!s1v+^8rCWlB41^y2BJ1BvFClI*IGCQ6NGNo_GN% zEt9PEy5tx~*4C@O^K4L(y3^-XrT)ppRtnL9S3^y52_6yaLNy+d1tcliwFyo(8GKHjwyrLGCe5{>B4x_XTzE8?S2 zEoPdl>QVNq?ymO79LNQ>U;GASeOP)IKf~_BMvELb{D7=r8-FnK_}DA z%#2jF)kxkVAb<&UJViZhaZM&`H)l;iP(CFZZbE5^k<0PD5r(2W-fe0}2S$Nrf7;O* zMaI*V)H?f&R7F|`yB%v=rGy~w=b_oSb(KM53eUHl^IHJbB`6AZu9ARrOIl#dPTtlu zJ=q9}Mv-YRaD@(Qsteb5G_8Ij$9X>o#(Bu_IGCJt-X@=TCyj};`aUCApowWa8f)9r zs%0dR5raq?)lDQtxjmVRy+e$84+utWpei1WmaV;y?kK&~j^gm+y)N>9{EC9ojRM46 zxlx3hjWSh}#1?f9>RY#avM?yhMZH*|{Vs&{`vV(y_5vHrh?RL~rU*0{MZ$fy{WeqNtC;{?LNe zL>x;qaZkMJO{euh5ajlg3{)Ej#0nJLRAlrdLCA(=CUPG+kYnl9DD_Rn)I6sytj?Dj z0z&4eBdxkbc7b@Ug2s+`2uQc53J`6Xf>f>gn|IYiHhzj*_%M+WPFlUvRUfmdvNV4lV0y!!RS!{hVQ^6g*wfgyiafBE-6ZkMaCufM+?z#nhly}$nA@u%fmHZ#sYKZIZS zw9oYR_UZA*4>z|@3z59M9n-yee0aWnU`0Z3f?r>M09)wE8+7`WqoDcg$E)AJf5%oA zo#5@Vuk+^dN1k;*+y4{DX(ry5o@UYkpXP$c!QWgzU*A1`|MJnFKHPqHA$%_V`1K7bvV8ph-6!2BS8pF5pBEk2tbX;)`~Tdsd;3}F^@nFW4(;qvE+2jLn?HT^U&o0} Ag#Z8m literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/Contents.json new file mode 100644 index 00000000000..d725e5b17a3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "coverlogo_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/coverlogo_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CoverLogo.imageset/coverlogo_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c1d5e17093a97952956bee920f238f0841406530 GIT binary patch literal 8922 zcmZviOK%*<6@~ZnD|%zV4&Y|JAAn&Xmg68uf;e(^MivY;wiQrhBGQ4AU!U*X>aK1| z=4y0Qk6ZU~?qlYYZ@>QLbHCQh#V>Ao{Kr2p%ksq+%a>nXt`BeY&r3MtH-EZ5{P_IS z5&>Up)zkC)>+QSC&DYoeyuG`={{2_WtMAVLx_i3(`{Gw;e~a5cd_F(T|4VC(sTaS5 zwbYbiTkhUn<}$aY+EVn(T>o_UaA|9a+4;PLu=ehX^UFJdC3D{iyuWx({dKgb-p&WG zHm$Do8v8sUEk3V4g;0-E%i;^qi9SVJ7hI@*xx0Y2+FY>RE%CW8zOK2mxoTx~K9;!n zwuV?*F2Z7sJ_qgYYviDgM1F0N^SRNjzVtLc^%7%G!m1BBb537duPIk{v&SV@3-b0g z`z&)}|&7RQfH|r#%}L%j9?z-P`7zyLJ8PI9##XFn~&fG36CDW#5vqu z*C1D8rC*av2qJk)s|gK)WwaK|Z(8Ru=1aM!!W-C~SJ#PhJF^s=4%vviDtl@-m^|nd zw-dWNh4=*TFUW&?cQfd=yAWcyr@8EQNnTUTzL?ZgnzZtcjIf8;Y4w54Cg1kDxRzdU z9<5SHnHs{%r@-3}eT6mn=7z`kO|6P2^eRi>*_-~?+%kcGHq@GUH!gm%=NfzN-pp_q zUXhx;Me6P$BX}|P=18c1Let~W`~*({Il;DepP=EfS2tAXrAT3{8G=+ymn(=4+vF|K zR)mmPwLJ~GJI1;QHD>?%rYPTq)S*JBiY7PL3j9l0brly`TTR*7o>gc7Ie zA_nuJ#${SKt}d61=I$|*4u43si7V0Hu$Oljphl=q`;;UR z8jV762?kyRIWB2mSjksGLAyeyUN1)%F7p8kDivn(zTVIB1fRj+QiVfN9XgjVog|r9 zg{BEB35+X|ogl}Gwj4-@S1iZvr9+Wt!Yjnj4#frc4Xzb;R2opQ5)3thuH5R7GcO<^#04Eav2>SYVZenP3p)XR9k%J@?#{8SuBBRres3Tv%y#-Z3%D0lijo-^4hU zKO`ymcZ(tFq8(|(C$j0R4XeaEg->7=WrR+eFPQgDL@&jawx?q4O*Yk_Ekv&zJi%(b ziy$~pf#S(Z2L$OIqTukB@(DYF4vCM`C_#ogPuETidI@W$bX5g6QDxQfUT9@I@PwUi zKA}^@(~0vXr@JR6Rj3>ulFvimoj?rW0t z&de((s*+K~OJPEU!2zT-G7@T~HL!3(HAStEQe$q!9af1HaYa`sy_1t+Ib~%Up#W`r z8aCP-vr{GKkp0NU6`{~7Dyh^>2`H#ZA{1~$3MJAetdwXuHWQN}DmjURsV}%*w^FQQ zEj}a?mq8FhAXKRZRAyW4AzPAhv@UsC9aO3!Ad&xoRAi|hfftpl4I=B&A(+58D&@V_ zdCS{4I`w`>P9Vp5ycH;iWRNNu%)(YtB(Sm@M4<}TSgccL6Vd{aS;^T}S`%d;d3J*I zCu1c=%mm5J8;DYEdv4T%9niz26*a)16znL3E)o?fFo<}ZfPRwy3qg>{$Ue9e&~b)B z4KEp(ASn>tsMO8Cf*#SfPH1x%u05?odDayOA|a|cY)m@jE`o?X zgXsEjf^F*w#2auIt#mfQz%C@tv014@n|o}~?KqEGT+&f2%CzIWt|C+*yfu1Vr(V!B zPw!5}OI@`&y;d1yT|CRXI_e!{9U~jUY(Spriy(Ya{>G|G%fqb#H5ReTxzGwi@j!{a zLtx%Lwqj7?1)X$QLkiG3_)^mv2o!D_>!{J~tt}<5i4g*K$laPNnjUNCHxOo$jxnP` zO<*BA2&|MgfP!#en1_CV8eVNLMKIGfYY1={4HQfiY)xdaF2W9)4qyp_OeD>*gx;_O z6%&L;aSC4`)7YwGI2n-MOir||WK23DK!-#MRZb=s zqJYDV)q)S6Ixsd9{d7=>b)c$zgIBM@bq+5_128I?3ztSbF`rF*;C&ij`YIgWd#9nG zSf$d70P!*u=553iE#0)ZSRa)2iS%W>C8?YVBoN(L9pe3vZxZJ4vVPGw9K)|h|8ST*(J)B zavr>+I9ev%i>GPqq~Ssq;iLRQ%y2T%L>!t=atZw-q-E(&?W63=VAv>~I;VlU}XW41#=1Y`8WhaCJS;h4I*2wot1lVH4cHNOb`a2ltO}-<3Y%5 z#T%G7=@o#440Zv80i-AmVQ>K5nkZ`+ln7$nM^59zklePCk+M;Xio~&1dDIAvL4bM^ zw$iKP;%ub{>jbQARTeTGQ>^7NNG+rQOTzF4g|4uTne8X6~rm&sNeWRS3pz4JX=OK|C3w zGn$QT7@|0;2zeB|uL?t(Aw{c0fUIRTNR67Uq9)qUVHo}>67CWb$;RRg|k zUCa(@It|MZ&=!k~12nXO0hPyaS^kH|ur-tA(26R(0(WTyo)w-N?=v)k;fZV@njX&% zN^AUYyp=2P0IE6I3wF@Mm>W=DLv2)*___)d)n(9)`XwB4o8>dRV#H5Ypfn_ZtE8YX zBZ{ag=;0Fes6mWlnNSUn}O(!YPvRsf-9O|J#Nhnk` z^r+4$3JMoc{_A?SVk*^=RShy`7iq9HFwoQl37&$h1ksc7bqJ91tWhc;x+Nb;dkirZ z9yITKIfawq<1{JIEuO;Hn|h)?RM=g&!gwS)Y|8|cY&blHaj_bCt{~B?<6|s9U5d}h zjLe#?oxr0gujpg})q(L=ARATz!Cg^m5`-&gQl~-p+=}bbyJ&2cuz;=8Y*i17RNjeLE`zzlN02HH~tGOShvsM<8QQ&FvM|aK+ zt)s`M5|+199*ytBK6DB56x>X0vY3(jh0&Ni#lTU90V=&11gVJe@FELQR@KIsUyvp# zTQ`B9kk4&h;q0-Mv?_ArG|GShy}VVzC^j-i9m3FzXe?wPx>Rz&ejYU#LWe>UL^ZgID++EJ80)|V4haQO7HhM5EnV~BdAjC@ z-&`Ipf4%5u2v6fT2Kx^Z^xK2+Lj_WppTAR}VK{!jaP!-ThsWoq<@>+$dj|e){`TL0 zU6-40Z-2aQz@M*g-rj!o_{;J=#l7a1@b~->f92DD((CKfc|G{Py-e*g~sq(CJ4Ng65AcZvOc84Wcf3!Ruwe=hfrS?DZeX{{(V6i5Ba0 zk{h}5e{_)3`TYq_f{o%rZ`!b$yDj-S-d*JPlUfVt`4->+xUP5pXs?gtD zMV0i?DmA1ZtWrO`L(lJT-`-!}U-Ge7eggpZxAG-~1o1f!^l; literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/Contents.json new file mode 100644 index 00000000000..e2029cfa220 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "backgroundphoto_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/backgroundphoto_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/CustomWallpaper.imageset/backgroundphoto_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..13c5740a04f449903914752808f246598a794495 GIT binary patch literal 9789 zcmZwNTd!T&Q3c@l{VVoMO9@i8*Zm?zkw`Z|2oR0KTa*Xq^hw9a7qA@^`1O3F=GvFz zZVM6j@VwTn8a1lc{^tAde)p^XvYt-;xjTRS(|?}M=U@N&{F~pLox}yR<9OudAzLa*ze={>HX<8th-F>HjVxHHvaR|htqUf=XL0obH81NVcEL%{2sV% z-PRd;1EU+Ba z*QvADP<9^2E#szr-K-X9?)N3oFmIF90@*6NvdOZny}S0TGmJx-i5)yjpvCdJn0;bG zQ-{m8OvAj?)Z;CN(RzKiPRxFLGhpxWP4==D8Lc;7pZn$kcj(E~#R1m&#khI!$Zlb^ zahdzkY7+yj?t0rbQ>SIvGS2zNX_?~zz+v4xBZtf4#pd1QabMQX7Q=-*E&UdV0R{#g z{NuXS)f&jQS8!_MmN?HbE*CR(S-Dt)rj1pA{%p0yFpulFWE=-=2kD1x7zWU=tb5Ep z@IX${ruO@`=W0Tm2lScR4<5j-{bd=wKn~n>>pa)<%zX#7>&PwEeyFL_?&&uHxo^wZ zoMPIxg)e~CnP+7+Z^JKZ>Na~@2hMYk0mqArPJA3BaOOFyO^e%>^9;TFw5g-z+Il@x zbIW?-g%_FX{XHm1T<=r4VlVD^XAWFotyf@9S5UcPAmQGiv9486b{#GZ=eF6RTiX>c zYo9w7Bw%u+%e1Xxw*|@(W8P_Usv?CW7Kg`(otLF^>j&r-*^l77ul(J(`)UVIJnnkq z6Ta4M8}|#J%~iu*zf$$YFF=^`Bs7WnNuV@k;V{qH*#_Fxms z6{E9DAiHp%*CpYJDDX_6WgU|PK;oq@NUlhlsqkb({9xRGwYepp=t6x-A3Of(3X;}D z7HD2(8Lp;ITQ{s*f!=33NCq9r5Z8pX*41V-&uTy`%ZoOd_bsLZ<$BqA zf6vPXJ#4fJjasv;fw)5Yo`^fLOy*A>TGZ#+y5X>Jl8A_?ryQoetPvl#{PDmu(#~w2 zvCBL6M-O(qZhWXDSDh&6-1cecF)}ZC8+l;Eqc*GAeJ7N^nk_>5=5UG8*f)uYW14-t zx7kqIBGCjze~lD__EcI~j)~9*8$lEA?!G1#I`Lj8vkw#-)Vp;dSd3()$=$twzYP2Y zwys#U)v%LAr8jyxxD9pQ6X&ZmM-~pyhw3l-gm*ko;552#nePnP})TKZp;C%HG&woDV_e;g^wxP#s#0B_mNOf0m>u z$C0XU_kM|9M{b1hF%m5HQ!(bEADv4&zZIX}OQSK-1!cVY$39H~`8IS)4+ZRT&BDEYff9$0!}u2}e>VU5Ym-n?b)e`JtiM|?80HSf75ypu?I z%t=>%yJ5*{>=@rn1ntf*WiLui^Tf&&f4`wUX+~UlFPMbhV-vZ*$6%#+5N?O0pH4tB zq?KuM<0_GZuzU&}#Uaj7LOenxA&;rYIKowa;5w?<-CuZ9iSHfzquq%0lD7+gu>aG^ z@p6%bwIJd-N%KjyiS_I&edcoa3*Rxbm&+pOhT`rcsV?`8K<+(axUz_Vu1f_X*pz&P zOF2JI<${Iws4Mc#2!1~0oXzAhNy*TnQrdW^3I>+K)k?-GhiHf9)I`&Yu%H5~qsXKg z_8+5vVZ18EyMVDFsh8Q2x-}aCM3gzZbE2ap;+ZO?q0H{l1L*>W*?Zn(Sk}UL3$sVu#=!_AJJDe zngA!a*(c+=sV^0tupiTtXef!WAITM!#s!IZtV6ci$KyL(Fa(<99Bfm7vubz2sq$B> z;Y9LZY|XQh{^}joGm*c+{E6q($-5A&?gj%@tz0P1@)AMUk@RmtuOrdyo;F43((c2A zN$Sl~>|4prSty?69mB7OXYGSbXwO1NlBvW3A6!~f!#xP3%#S^?yOF!vyF8ep6vTxo z2CLpMQbn&BRcOCYSx3sXW$4t|?IH>B#(C_jBM@uw>hc(|t2Fj^BjdtNOz;E2U2oBd z=^ndzwT6{6N65-@by-EcGHXX-_Od`&2kIo0P3=td3+_rB=!%bOZ3N80xp9@^5`iPgKLK-ZjJ)tQ7N!*eiDu)r|Kxc<|YLq z*9R{VOGd8A#LY>T)N+Z0h=0)GBcSrYTkce+t7w?Fou>R+rITNzU$| zW6q{2)$kV05kFRwdscNGX^Cm-3jhHjJK0qUN-h{Dw$kAQahEvM&zPwMw$~suT5;kN zK?(>0TTm#8<10!U3OXeQFd1+?!Yz&vV2Dwyi12Z2`}G+Fj(leA#9t^JWAK} z-H9b+;!_Qq`5abf@LkRSc>%GMrh-6B0gwiwYcq2)0!78#=#d-Hb>hgQOXG1h#HPoT z6oK%C6P8d5JG)RMA;Op`19jcZV-bXAKbTsS=6wef4Amlz=8q1R(a7eonQ~b7BnbVi ztp_2w6Hr-;eyWO-##f|zXqG3W<^AaKQnJakP7_475^0&ob7RV2L|AgYEX7$;WC$2{ zZ3)@Elj!v9R;JCV&_NF4?5tI5$+9F#VO!e4SxXl_U6+h=qs)w?O@|sn)pLz}Qov&V zqCMy7nr)_aPex6>wbub9>@_uatH8?2_%zb{io~U_etyNA;T{iH@F8qK$ke%R@s;mH zju^M(@Y4F?z1as#C>jNHrx(vE#ED))IwA@C@+PsH#$s7xPOXY?;!dPcN|r!;Iv)t5 zh@v5GkRZEu3-2J`PdAp0ZmZ>nsX!lgP04DtDw+&Dj+clFe^rbgw{V|sNzgr97k1D| zXhuoS`D12T+&;bo7R#ux(CC3y%hUVm>L;gUq~udJM94mZ2J>k^F%~1DOzAxtDU6Di z-J1mxG<6UgCgNdSkje@(;H9W{vy&I-;SLexsTAf7$~O@9(SdC|o}eey5J76+a-y1= zuV6Rh@`eW1NGg-fMFI}d$3VAznI5sWFRB+wOjf$b|5iCxI_2x%JW=&0#_4BKO7%2_;*bAn+ zS5eH)spKF6A*uk+tBx{;=*Rfx$q)(}K1duPI*HW}BTUzm6p{MJK2-ZRgvX2!nLKUs zOiZ5@`=w2YEu(%npJgrVNX~4iO^`5$bFZPf)Nc;8NHZlr)og2zce9`V^UfxXPP0 z5&;FgCWsf{Pe?C<_0NBI`f&R9Q-0}l{EX-No1gr=r~MXa$ELTR0`ZOePlR6m{f7@9 zzkELb;g5bY~{hLp)v-m9N;de`cZog-G_0MmAaMW{d z@cMk+=i85eHMjrC`9DJ*Rzf)sE9HWpZyVk({_UGDZ{B_U@#WONeR}@$i7q>}Tj&3u zX*YomKK@Y0^|$kf8x-EipBtnVBA~y5gy>h0e!!JmrrlrzI*<3@&m6||MVSm z{NnS+_s_pP|KjZrzidW%_4?z-FXvpi4E*Z%Z~yPv>FbAHZ$5pg>xi`V^vyT_@Nd8S Ef0C?-;s5{u literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/Contents.json new file mode 100644 index 00000000000..88bed3c8472 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "status_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/status_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/EmojiStatus.imageset/status_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9b082adf4a9715640e9e1290ab7a1178b476cabf GIT binary patch literal 3542 zcmZu!O^+Kl486~<&?P`pAhJk_`T&9e&88`ew&-@#ThN0$J4v?KwOiXM(*F8BYQ`FQ z6CVQQEh&D8eB?a2e*NY-*)$AfoZ0{J=U~i>7v|;5VcOm5e+TaI%}>+r!|9{(0B=<4 zcsfkmhhg)2`sZ$(ZoYqIuHN?l8;`@^gR{vca>=yA^LAJPdh<>1NC899!-Ap=d~$Z)hpB8!-ZA=#2CbL_xmCF7ndCjv9h^pKOBB$yGZXSrar ztKfYHI!jCp$quA!Il%HIm~2kDLSOTe=FEUvVvm0t7#CML#Bje37)BD5PuWgh-5Wr9B_-o%+i6u2B~pCZn{JM8QBw*rjuD)f`nNc^(Q5jn$*lb zOIDA(mT{irkN^sTZwQjuC)hz{JG_6z95GYX4k_T>)gzP0p$ZZ9Tp8N)}#F{$aIBJE^`a1>RGPq`v&%@T)}9U z0dl{pV<0VqCffp6G`o$2zB0DP;iD!>(J(M^KY$6v1)mMlA7XJ4H=`k92FC!B*_D-H8#H2n zC?!=_8rW6}kgm+GtWe{1RnZpA1fQ@lCUi3 zl7W3Fu(GSe=G2hN#u7^kiwsI(#3Hqf&!tAavKPh^LJZY_E?3n@B-WW7WOsWS*zv63Q6M|Bvjj*`Pl11`=& zdM6lKhbih5jZy*;csbY-GVA_=7RZ_V)&yy9m7oTB3J!)v* z+8x@;6)c%OCvfyy)g4Q=N{1y(P}Hva*-*SA#--!FoxaxYN(1k|8Fs_3gRbDqetllX zb?=^+bS{qCjvg9}-F@@zZnr-j&AZ>R%j38C?ccv9v$@`Wm=@sY>Gp2>YX8~1)8ebY z_23({p3XPjOvnAF!#Ev{el6TA*In&*r)dX^`l)aRueS$aMi!|1piroNXl#DGyM?Pp zBe*eVJ6HQp(4{Z@KLY7)iWfI&fX8NV4g6|*+TQO!T&jLPOz#JL0}ONc`mc%DdBgx- ze(2JAGP?$O_^H1JVLL)9E+90=V=2sdeEeKWfo~ghI&AOmr^BGH_rq(XAp literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/Contents.json new file mode 100644 index 00000000000..4827940cc18 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linkcolor_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/linkcolor_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkColor.imageset/linkcolor_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2d2a623cd85ae5d74ff90df07fe032e9b5ef09f3 GIT binary patch literal 7498 zcma)>TaR2v6@}l=uQ)G}5~Q4|`$dW(5t|?c2s7a=;$dia93y)MW+o`)*YmA?s;iG} zB$x-Qy}EAuw)U#(&tAR!_DjFjW$~+9AO80DWnI7eYW@1_<$V8I|6Rfnzxl)Y{>}4` zYXrR2sHf-0^WFR9=H>ZcZ%^mj-+r^c`0n_>)6??L#c$63R(JXE<@_|iOIv8E`>@6> z^x#@rPw$sG%x$T*6#Y8KKb`Iu-?o}cPGR+Zt1-n^*7u7qTaVdSzXreMlxxnbk6Z68 zmT<(v=3D7K9%i4GzLf#iE$mCa15BZYo>r~RmaVNFTS|!i0FBeQ)z7J?rERH|rX$uZ z_2gPyk5Kp7#|C7EFtu&IrpCIb#rrMDJhuk76`^%~w_r-HHM+Wn$Qg2R9II|Imk`Pk z8kh^DXw>5kisxTLERO3WKUBqWQn)^Px<`TWR z(M2t;+0}LYxQ{c;W=CJLKccMOeas3`+gdLv*g6I0vLje-_{_;0eak&GeGgiTZ|04@ z#o9bx1tpfTSu(g@T*sSy+whyM^hv6mmLFwND9(5G5A2VVGbsd04(7^QajPx6V#yR; z$Qh5Mt@)G`7r!OG)9LfpN^ym>T|jYIZuqloE_CulBZDH%>_T4o>WGUc0vRU?OWpMx z_SrtPzN6L0-iZcjul%+Zdu^Rh!dz+cCh6A6lf2J%j!;^@Mb}eK@YGV#)($e zHE8Jn4BV=EpUXslK18fEm1 zSILJ~JLCaYdbOGYi?zsl#VIClQC9s>zI#MD2@zwqPtI4vd<+?ngSi$$!1}BVP3&iu zI_6TvppoU3LtWL?AQv*Mlw1?vLOrS28523+@~j2&j(TULgW`ys>Z3SJGmMOOd`mr( z3#vmAAjza00j<@OF@}=-T8F*6%bwX}Jg3}e_m;Aww&GS&r{?yPdxwoy8wpmd#xTCN z5^SH4LZ^!4$s9^#?Lam47RKyP>g^qMsOrZ&)dzJ*1X353!;7T0$MdY=*_89OjZAFp zDvWdpe1Z{T2$_?D6_>Ly29-A2!{E2DAl=0pjCUk8BbEWQky>z&kSPRF8{YfgL#UqViBsFqE{q2e`L*h3G16;6XLX#*Ss9sp{o)~*wI8Vv;x zgu~StPD|aWOoC(Ukx{fpN-Hs$5I_QyB`&C2haad_&PI3eL{hu5K~#rx&}4xqwQFoq z(IqcaC`4s8pV>sM_mE5KGEf7QAJ`t++ndZ4s(TB~7(M#0rI%+9(H~aO(rc94yKM8t z%&3k=p&}LC~zSG>90DB2iVn$}F%DV-*z#vX45ojOZD5Es6*cgND?U$dgQ$$NQ8o*^aCLgRiaS znIa6LWKMyfqzxs5=-WoMe(SkedjW&QGR9~}n2j93yPaL^JcVdca5g&>c$K_CM8%LK z(Ac||O0x!wg>G(`1;A+0B48({n$TcsCp7q7f)$0#bl!xJw8Lj3dNy}DQiOpvQGY@Y z=Sq4OY7r_>7Bik=okekHU3?j>XS z#t`DqF7zKZhryJ5zQZ6|6h3p15XQ5i6b4LH8f_H>-Q(prFoeLsDw2xRgH;uZjYGmY z!_Zw78OvNrORd(CY*Zac!yG!}CquNum&Zs+#Hz69)Ts#; zLXpxZnOl)+lUE6v0!ru06o>4Adn(S{lY{jURJIdH!Je%4L*ja5A@<~Yt)jX}-BFPcZTplUUWoxx~sQcvb(m<-}3V;fRMwxGvIIOfrg z9UVCVa~)CXl-yf}@6w({QP!-VyJPJ%849k@gjvZ$ri!=FMKY+*+?$=B`j^pZ~+x&60 zO5wn~$6YykAWw`NC>QYn=j0C5|kn)7hu^_(*n6ViG#N4nJqR$7S{>+)?uA0hurG9ii)}Zbw zDJYmm@R;VFcX0cg+vW8Oh-zG-GiDu_w@g zj;oEx9(SD?0LbN@D}bei%lpF$VWAA|d`@>*swcAW_OnZnaiXtUzu@#7u|E+?X_uTku{g%&<%a+GD|} zYh7GohTXIsi`gZzSMSUWn2;$_dlG;%?Yg)pKUIn-p)E+0aj_gF!P5n$Ge)9~(a?CZK2qK(LHogUKbPY zXXg_oEw5iO>_O_2U`o>CNLno}((acP)eiK4B5dq!vr%!FB$T!+?aeyeICy2UvqOxM zf)W#SA0mN0h!2v|W`o)Tm{1aaJQwlIgizo8!JE#W-#iQK&p14ckg>WMp+WhxCzy+e zK^=`xH`y5rVY?5&M$A6I#<%q2>&Tf0hZSeOfL-=JW)>=*YSgooa!_xzKg=zTmC(Q!?6%Vvh#A0PD8NPSLZ*0bh~QTQKP;C3)qB2H*+UIpfUAiP!C6BPF| z48#kl=}sj}x~Ut|ndp7=K+Mu@j)(V#tC`0ef!W8+RSzLNf8>bvmlp!WtM{0UAIT7q z-JkG9(-Cam@H!7UFcrqn$jtnm@tcUm@QCunW+u#)X1*Nu14EC~K6E$Wn$cwhcXoor zIRk1^U*UajEqvf6sb@;!mhCM0lZ|w7&S)=#TMSxc%@OcP^G9Oxgs_Y(i@qHGb2&`~arky7nK&a7vxOi&1LV-^*@(eSiP({Iq`mSAIX^@8&Q6{>OQ} zd3E>ZJb*u*U%$Qk=Hch{d(JYhx_OAJv-{> zxeNH}?h$Ow13LVOCush-ck}zVuQ}=}3vSoTJ}(}AVy^$>{7)c-~hN>klE+F^W)sa}*EtA5TPk z61fjXRgugEdVain`|kX>@N4JI?_T1L&z~ONpTAf?fBX82-6=P>4-e0)3^oVfeE0U3 YGpDy-J?|c$%^XBzSw8#hH-Gx}KUl`fb^rhX literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/Contents.json new file mode 100644 index 00000000000..3d4dddeddc5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linkicon_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/linkicon_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/LinkLogo.imageset/linkicon_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0f88db3bf4016440870705d9ed936c6a5233a4af GIT binary patch literal 7604 zcmZvhTW=i45ryC9SImn6JAj*gzW|1TSc-!n38K(>GxET!)-nT$Oh`I#^6T@R>Y3q+ zk{*QR*}l~ER8{X6Z(e`@b#QH2!s^%ifBbV<*KfXAzx{SO-@VnJOT5Id{&c>3_w@6c z0I#*`@#*1w^I^Gqeg4nw>3seB@77m8T>f=>T>iZT=k4F>_YYqmpN{Xci#?AJ*W^M- z-naGiVL6t0ms`&%tjGGt)7=uH3%!n!)-c=%Dc8JySVD4R^i<;-(Jbbk_`bT4N~q%! z>#pTmvcK4UTB3KQwBg4Zx(hyfPQSp43$-ArzTI~xmr5F;tfwV3S9+Zd2xt~-VE1r7 zawkSY@7%Lb+C5y|Y!qFdHhRBAP71A+wua2w9Ft!!FiwvtNTOpmHt5naPc2iaG3GVm zt)6S=&g4pK3BO%n!NqCe%iYh0>(g)*6Lpt6s&P(te&mQXpCL@uQm6rFw|U!jIW4H| zYfLz-xlmH8WgRZZ76)FsfOa!nZ2epSybtGbB zxZX-7=w$rhGlt-l+6Gp_3&E9~Cm3;{LLs=EQ)OLu-FJ(b;L`BDc_8!%=)_iHKgP&q z-PP=4*ImW8*aja$LDBqdm?IBPGHL2Wpotra{m!z`yrkvOQi#;PHaEztYDy9HVJM87 zVxo|Y2pK~s4OmAW^+B=(SK}z=jewa4F%(>noh#VNt%XZ4Js}olMsDquE4Wp$+A@dW zGeS1l)*6vRMj9@rj)Gd3N-ks(j?CPKZ%O=-D{1)|-<)t63&@;7?o=mhW}^Z?IHemn zT8IiYt;8!tGAsF(Yc>$p=>~eZVgozk4Lh^<>JKmws=BBn@D7$v=owfEy?N#fk!Em_ z>%JA3Zp$c(Ei`UEteJrFh11#ZaTl>75QpP(`7K}sYy`>mmIMCq7%Nu9Na#gM%L@X@ z)v9%A^oTk@Ikki>fqZJ+0wecP5H{iFOeoE*p?5fLjgwQQAKoXs@i}qR$%2AN7^`Q> z!;To~NUvkxX@Tf%eOwDvKpgk=84_PjmJwZPr#T+MMO+kw4czRMkfTB}b}NLw6OZ&a z*)Fh10-Fxdk{Q{k5Wxm{Hpz-CB!l=q)?9VBY!FH?jHnqngBT_@MZK6bxjK9i`9_j? z#72yR`2dj$3)mFe(m5kS*fZSWi=f@yO#*HFkbUulIp;L)vBc)(UiDC+JE)O^`=I{`QQ2fNQ^jhj zl-}$JNc5<9C>Y?x)>NAyq*)*jq(FIu4kdFspb=R|*~r8vmr@~|&yfU&RA9hH&K?LH z@FF=ZxtmLPDO$7>_KCeD4Yvhf2uAdVh=P1a*tC&lU8uRID78t9ic5_4d`<~2HJifb zc+^lenxNYSRz9u2OA6QZD3%Svy-~tF~wh0uAgujdPsV2TNfe9?}C>()Mq61KGz!x_T-j)BxBTRBh|1DsUT6EoI*7J?1y47*%j9Vb-8~1JR!hHfBSD z0Q;~x!{i$=4vH?ME|AFHC={?^Ap_}d_U)>m8%RLfnU(f|^9f1il#xD%57Ht#dK|BY zghGnZtWO-YIOu6Ia&d+7$;V8iW&R+U6A_N{NvLYBL@M#VvX18Gi^;MK z^1(3LI@Cv4N%DPA5HKpO9d&}{f)s``k|k1t_~^2!KC?ezRUysHmA)gUl@OgA2E@l! zb@PNJpGY=?u%eQmRP~jbtipxC2ol|j^L_9L1rgGjsNo>Z>6x^{(RL(pbtOd1YX`(u zdT&qVM$Tg$4G*cMMi4$iB(|!zr6W+8B2Zve-P)xOjsv7f@zJ_zv9tskFagFq;84v%?lM#CVY zw^ac%0pUZo&htcc@+{q5Rbin+q+uXhfi~MgvD0=3WLsk)v=jjs>w2XpF^Ixv21o=V z7S;tL!1K11CX=%T(sRqkTtUg1+KyT;Qq^EcKsbF{S6F82bgAhY zAv?1O-J?=(Zp}>tooq~=8?Q(;s+2ZUBkMF&V0b_%!Wn8;eZwZndT|IE1t+CrP}*OM z07fXdSL+B6O=Hvm+FK1dKpDPgv?L{|H@n$OQdq(z^nf!`$OH^Z+`&tvmbTJTC-u}6 z2>W1K(5Fz56teInT*5kMV z0v$1dC@V##aMXxmX$4q3jh8tn*%^YUa1?9S)xacRPc!VKngJmX?jc@SM|2vm1fE2= z9;i|#p&hm7yc$iYb}EL{QnzOfub=hD3a0uwy8`yn6sQ`ep3P1pVhHf;)Vdj)aRod> zNmT<=8Rv|;c*S8X0O}NHm9B$+4+tKx(O$7|fIyxS%VnIVmKZ{!E2X0(4d_)!$jPBR zj^I1%edy-+XwOJcjL|RUM+Jec3~wO)Ypope+W3VrlENRb!j2Lmt%ugVZi+6)|MZhCm)LRBHvQPneK(M?kb# z8>I2dRxvN{w>E6LfqJ5_ts0!?y8Tw3u4G|OQy@%%BeAQT@PR}&T{FEn^NU#TMT>Jl zY^`)kya*nAP)2?bOId}rG7ySIzdKryhUEKl_k(8$!t-%??0NePXEPxvulAMdxrZ3&RSa^}1HMX)5Mx6tS-79MTE1De3fF zNaZkr0V%8SBM*i=@TzqL8nhTuPfrAs-tGo;+9R@Y6V+v!36PNudf=UjxRp(KZiD>L zl{4u0`2d8p(6XjXj?>GuDa+ekRoH7eeA@@G^(k=6iZ;)Q^AjWHFK^#TV(p99nL zl0vS`DDZfVIk;jqxY448qNBdCY7(Zo!X9k3G}Pl{^SnS{RN@Ar!9}!Gyz9+(5<&Y&L-C^_s#ep7s$019BNTM;u^Z9yf?3*^eF+t8oPbj9Mi< zZ1M)C2UgnsS@QrX4J<%96y>c#>q_FbDu}A7*8xGypb88$e%^}5i$Gq(c)quFlrVGv z(vUPEPMgoQG-^bWd7P4ljajG2oa-P%B4Bch|G90-K8uV!rhC9@{8|f?k zOpk$F1iQB4JbbIuI7Wo2@TZv03qHElgwb;}^}#k1f>;A#1k&u;s6acfRG|ar{S8@l zGq1l`Mi8Vu+ZXiM1`4>H#%fAvWyG$aX)%bQY-Z()TQ_0wIF4dvqoW|1Oc=lj(P}WI zxwUFVjIokn(@KP}a&4P!U18ud$`Qoyt5t#aR_2;pZJr0zRI@>h=JQ^$+FW%hiX{B$ zH_P4f*F`_DZNHxFKeg#swfPfVC4`S(;PL_U%iPs(@9yrO9@iiL&M$TRyZYOI|8-uk z-rT%9Z@{0=Z*OnDyZ?3l@%i_*<9D?5nNRyk*XPIkUmi~9$5p=tUT?>}y1#om-*KaU z2;9LpHxFQI*`SM`3k4lNBVPUS_AR2W(%^dC?|F6q3wy&S@;`!HoK!zLNeVt57d|Wg z>gMU@{r$U-xBmKY{%PS6Z<)V4>i<iYivX_dle;Hw{Q|93`u`+fE1 S;mOo#aO(2K7r*<<_x}e|oaJW# literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/Contents.json new file mode 100644 index 00000000000..9c449e53d0e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "namecolor_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/namecolor_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/NameColor.imageset/namecolor_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..55e90a4ad0ad3b4d818e0ce3e7f675d090a41637 GIT binary patch literal 10475 zcma)?TaP5g5ryCTSJVpeh0(xMf=&|MAbuvV8X0^7-eN>%(jP@5P_-n?GG2 zet7goC2_4e)M=Bw*}-rQYZ{r-#P%Wu#BclUJp_rF(i@)>2at+SZmzD}H%<$!qepm1`pO~d3Fgdet|gYF zku|pv!{W;7nKF7|B}QMfupGh`ZN}tF;#fRg*BEOFHqtrQoPDoP8(9A~v3BFsCAqlI zle3-%ja$s@h}pHGnJT5QwIVE4R|}sQml&;re_#AA%t#Rxs z3+B|SXFp{f*xd!RIBw#M3M+oAwoYr_`=+O>=jJ#APgiWCgpt|*28MprTMaz#p7*p zD+Qb9wU)YRzixH4`i)&=+&RcTU_Lb#*VAcr(et=aE_Czk4G+q#;4M4Ge(<#xTV+3e z98Ss8hX+pd{&G0$OE9F(ou9wI;8=O%-+fZdmf3G9*kWHhENKls*TmJGHXgCKOU?G^ z`G8djMVA9dWgO>}@xof`hQ|up94;X*1#DaDuFwjXq;YNAh&frdg_a3TL9MiC5MIgN z?k=)SsouaU7j3NSTjjIHRp%K9lxqupwi06U@@_Uv3=SsHSsm9!>u-HcbqUU3EL6y)^)-nCJk_r*aNNGRjf3Hnj=RXs1-8jb zJ{9c_lu%{@JssS(B^wzEwlI^162vWxc;NDawB>$roB^w*K&>qtp22 z3IigeTd!hyi+G^~7#^-$ip{u+z=GBzPjj2e{F-s%1M#-UGO&^{-5HgY@Dbb5F?R68 z2Pi~cUycpp`v9Y#)*_sW<4z58As6l_a&yGL7>ALB7(Ax@wJ{}Oq!V%8jck%&RoKi} z!3A;yCrZjm@)3J_D`Y)vPo%I;HBy1}a2;VSz(+mgQ-=J-nm&bW8HPJ=ikR7p#`}1iWe!QiM>S0{T_UH^Zty*H+x)+aVoBKNl z8zAfA;|G}0l*+V6`X>AJOsfRa(<pew8Evho12(8V1(VMGZN_w3$P4H&e|(qWBHdp2dv(2Ujt_99l*x&Edjwv>|1< z)dK(7t#>7Zrw-MOqYoiNQDC<(UDb6A%4tCM=#()NL`Rdaaw_MM2DZF8Jc9lCq8!gS zsYucCvh1J+6;ZC72s& z#st16!;>fs8u~sU8~MkCYA+s;nsId=Ca`g;F?^L|GM2QE4pGDmA7C0dhE<94reX!B z_0W3PMkAovc?zVlQ#{YOkgS0j__~loIcz8v%^1qDD|Vo0RSJY>9KxOZTq807y6WpA z=dkZ?xxPGJZZ?;JZFK}jN0bix;;OLrd$~grvjcM2f!$x)x}if=&8hHC>c^nN%}?WE%2~I3nvIb?a2%Cns*#R3xk`b=96-T{==rmCADAP6SReV`dh<(JElADMg<{+J7F`14G6otU1 z)VVU9r#vFwi1S8?pix^j0tX@EF%wsXOeDEXF3|Oj_?wtjInXTS-wEq1dr-zKXS55s zE0j5_8IYa3I>k~O=-Q0pjA2O+AcF@x!E&$@UHTO^QppBZlb6WH*;z&>MAtgBxW_U| zL?HAOZFP7Vb@sb!e}!*ds4)W-4FNeS&oIR;ob=vzhU^NegGD?q3Mi^wMT@??U5VG0_B)95N zt>m#x&R61tNH8?hLW#6Gbg)Tq#+*pggWrr^xz~f`I&(lFgWJSP zmh!j3EJNIO+y&tNCf{Tvf)_JGob~vMK4Ar0R_BMtE!;w z4X}b*Kq=}`r!Iq&ZN6s(fF7d-*iMnYZqbfnc3y?k~qGooGl9JCui3}>P6BmuD=JzDxY>I2f& zKsyJu_Vo$Cg%D~rD8{mJesvXnWA#EF&2r;VV-OTe zoh?=;hEz23Xhk(b#ewCK_4qD{mAf&oh>5hw1tLJ`Pg^WdOHs2?gA3#d*cd`25LFJ- zrbV-1T3n2dgEXx=x4FUqTD7Pjh}!@XZ`clqN?cVpP~`emR)}7ukb(9#Gd96>x&cO) z+0^JJCp?0+gAU zkD68cWW(&34~0ocOPY=C@~j|QSUu_ii3XL^w1OSQk%EQx0lg)wgXp!I&V(FAJB$Jl ziE34{F?r4oCeg~BpiqefjLuo9$AVUsWafl|T=1MhkE#tImjLY7sr-Nyi&8*c6M8_; zuqU*9cWwG+grwqLolcD+)=)^y_X{C#Ph=D-I(rpDLI@t6p6s;-?>DUVH9>k2e5rUx zebc3Z{SSx)$s{~_#~YD-oLZ~&5X5c!Y;;TW(BVl}S60Rw7JGRU* z7L0cfcD!ks7ud1VF>>1aG~>Jyuzl@`aYUu=_ij|Z`VWTc_P_tl<>B(zU;o-{6m3}1 z{(i-4Gg1!~2p%m4><>w!iAeQ2VS?~IG6$V6NX@yzSDfrt@rYkh-JiRi!?8on=y-4zL4 z;a1EvSJk8JSKUSJk2&ZAilTTAf?Vn6p!PP2K%P?{Pp9P>EBz=YmCALVVHIwkT9QIQ zV7K1kALcU<$)U^`HSgj)j`Mh{C6C$0&Ehh1)#(9%i5x+C@`XTNATlA=qfV8G1wwc6;ztGz97>Xm*TnqAvyt?~L>cgN0o7cE_kgka~Aq?}t?0$U<_NbA`JDOHMkt20|zn=r+Jfw6SOins)lTW-8#Y9?tpAjt3M6(@@ zwe4wXX-O#59~wrwo!lX}H!(>INBTz?Z|+q^avoZSIHf%eN-wXYIQ)38$lLz$79TN8 z57=C}Q3RTeGOAtbitaITTeo|%Fep+*y+EP;E`;^_0~>et0vq3w_l}X%4^A^qe*xRq zK6{p0S`;*BCi$SsOYNNZrLdGReAVhEY)Q9{bK%ucvY5Nua#(mq0KR_Q1)euzW!~8- zrsnM)n4+?<{HBel2BtGCWd^2*v|bbbiv=+iO&V+C4O?WK#a`J=uaGP&yx%l!I7byq z6Dzt&j+zOD#oKO}d#BtXbb)DJ4^P1}M%OSB_E8b1|L^z(WG8O?ey zzFy|r3ky%|s!rIbjfj+hp|u%4&`%KW_Z}5{=$#o-zDA7=6O{_jd>}KPtqQVXq+9*w z!-^n|rJ2-(f=f^9fuO+cCjoQ@IG}a4aaLZUaz154G84Iv9LTZsYL0@YVrrgK7f|O* z=uLt`T?s2hWEY56J!Lws^y&|Ls^ZWV?73le>I<04tUhjL`tQ8IiQIATr? z{pGYC7@Zx`2N?ASkqts~jZ0#mp5~dp!ut9Vlg?uGyVg_U=Q&vcYiJZdVNkzFnt(1J zks@Z0)n(${pnzPZNNL(#qTBv}#^s!$@m&|^ecZh2V>XqN<`2iJC%3@{@Dj{{cG53J zbWAm&;5<1X8l~u%NG=e$vJyDg0rp;qom8=YmNtHKw*NqlhsO`spvf?Qw?=pzzh}Go z?Zd<4^V9O(-}!wTe>Z>o@4v3g%{R9{TnF&y>+3hSUp)T2e8*nK@#lx|3!nCxUR|Fa ze|mR!eOf5;mRDoCFCQPCuMez91Wxdq+jn3KEn$OBKe7`ve|&fI$2YIp>Y@|8TK09m zeEf-L-3R-B0y)jZ%evD{I^fe>@HqIFx6iltk3YP8^yhci?=M8pr5|7aU1+HwjDW`< zfo7kUhY1naFCm)O+=>3?C^97*J3|uq@YQ<|nQw=l-`&2szrMThYs8yBe1$naetLX+ y{bc$0&FfFPQEpy6K0Yrxuvz`)+c*EavU~eg;_bU>aS0sbpYTMQZbpj$)u3GaHziN@5QvAq};+EG*ItDh1hs&_FG= zrZ5LkU|S2osFe{LS%MN{^=Im`VXQ{%48vmzhzdq$D6p1TsCI*y@q{?Ugz)6TF$d0E z*yv$Nt-%S0oVADrn3R)LgDN9kCf09zLfSD{L?Q7S0!!2yY~p5XQo$2ONU2TwO=T##%dCOuVf!r;3F6q?ffCTfWktPS7?DlZV`44bw0gqmceMT9E|Tw~3?n7mn$ z6+tDjB-n#`(G0!#NKhh~IFNnK(p7?=i^UQ$8qQL}3A`7aV67?UM&}C;GP(p{CPEM6 z9wS}M36lk-)*BqntlcY=V%^oBS^+SJx_lb!p?~mLQnsOh#|ic6_6QV%O1P-ml#6;Q zm2??O#GoRyEbirkir^3f+K4cwEHSABvoX|Kl8FWch!)dK#x-T7&Jzalb4W(Jk=X<^ z^&op8W~4v3k6*d1``Ot>d0rRi-C}Fxv~#Ekk1Gcwb6Q1 z&fx8856ojCcWAaB4I206;`_}VLiLux(x3fY?>^xz{V)ClkeMY+=b0r-@Gv@@62D#@ zSC6~<%cr0B!$%jrxKsL_0xSVkAMo>sz)JsNJ3`RzB}CN*_{A-%5cYqdW()N47Iy9k z9rvrvA3*<%yKVa<@BrOSlufc)$5)K=l0&#cF@->0ls` LySn=3r+5DX6vxSJ literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/Contents.json new file mode 100644 index 00000000000..d18b85eb13a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "storiescircle_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/storiescircle_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Story.imageset/storiescircle_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..50cf27645fc2affe7f46b202c708a15310b268ae GIT binary patch literal 4791 zcmZu#+l~`Q5PhGo=oci2#JKMl2_c0gL{SojEO|qDaJ&O-gzdyO5y{u*R8>#Sc)<^= z*)!eMbv;$nk6yog_0)}J7@V=@_{X1vG0&cv=g)`fa7X_RevNN_nhx(TA4~vvORLV8 z)3n_Wo0ro+_q*x#`xoZs&HBIHdH8#9qvfBm^TShp(syC3A-3#{KJ4~`F0*4SH3nyN z{dsp7oEu$EkZrslL$KBrvmYE5d+S52~mFfucDYq6)9b3&M@+#*_!rt9b$Lw=V znbz5uee_rtns*tHgOD7W1r&3Lcn2ti>}=qzDa7Kc@pfz_RmiJjtq{ggi7xqS+UR4f zDbcFxeC4fetQnglZw(1Q10#|x!C|483~?)S72A;$s`t5Gt@O|^X0ijybctcKHMw|w zv62=V(Y*b}P0UJ`ApmA=l>E${3rCjly3>n?r%GVG-PlCB|hu*<2Y*#if=B ziwI+Gw9e-oaVET(5g>1qHLi_FN74>h@KDqfmSS}kZz0h)>yVL+Y^%sz>@Kj~Rgd?S zNh!lK{mDhs-v&sOJ;I#*cnuE!UaX8gBc=O5&I2Bort=(M++K5_?p8bzE>{UuIoO|tl@?~l4eU1~n zKZ0>8grxHWXuXBzXAewavmQqf59j?~?-Dw@h zts!dO?1cs7-c9aOY2B%n^q@l>!a<#z!=}3R5MeM81E~2@-$qrbof7WUpsTt=vqF8Q zsRM*uOG!sPn==gi8ZqvYh%^CE8qXb@cvYcQpw}Q;2^D|}Tk52rxeg~6Iou+g&5Y-; z88xotF3bp?2Go0i@TZb0wFu!^P%n3y-c^K;GxIjJHe|H0c0>7Umm=rXHNpi)g0 z?SJY|^0E%alKvR;_n5@{?97FkFbPPh(mcjoh6kO)rNpt1CU_p#OD6&h)j|zWTF7D< z{fUf-Qka7J7Xm(an8qR(bpMVJdIA$Hlc+*SFdbPH>%4R!hs|@Mt4nTdDSDyjuL%+Hf3hUQ_Fc9V^*w8;B0Lh2cVyi;f zs-Gna>c@);`!bvqVc8JbOm4eSA#-NLAK~F*5 z&nxAL=vW2zw}k1**sUw&KzuGP0!7!WlNH(xs-a?Po61sTeI`CxL<7U6}CWYJ65P_x5!(J z_?oqp3h%xd4#Tel-7)kFg!#6CE*9h#f@V#AMfv=sv!$*&3w+y@e}sCFZ4eHSxL%QlE}cb9=I5Ov%PFT z9^YSY{d}6<4c3%F%%{HwpYNE-0QVnFS@bgpg#z@vf}#f#K7^1o51@z<^#PPt;xDJ| z{m1Dv;A&~}!%Mj1@%gx)o|wn?cTdDAo7>~@V#r{2@aE0^e-pIlE2iz~!sg&Ar45fB JefP_&{{gcPyPW_4 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/Contents.json new file mode 100644 index 00000000000..f6f2b9a3ab1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "background_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/background_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostPerk/Wallpaper.imageset/background_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a4b92bd23afae8cf7d86bb0b0f9b80dc3c948578 GIT binary patch literal 8648 zcmZviNpD=o6@~ZxE8a$c9l*_+2f#28OK}h+K@>SVBMXLVOA075A?d)$ug`aG)f-5g z8?_E=yu-QYR`b={H{X5b&-HZji(Bsh@z2w;eEs$E%{QmZ-FyA_6pr}SpDuSFo<1%S z@SR3IK0REnKcBANT>kUv=JM|M-!8AeKmPCL@$~PLKRf$b-1_0G?bG3RHMJb`63;QF z>g#gz`Lqpl=TuvYe%Z!9-rSw?x#k*NS$sX0l0%EjXTa86a}3?UeD2NnkOkEkol8qO zJD*F8f?Dl0y5;61u+$t3Y`NFE+yX}5oi^!bcB{oPIR@`KlYv@Ft}Sg_ub&z1Qf9q& z_QhA{G_J?kU1eOzKDW9A?iNzumq*CwUShYY`?%;zEQN7%HKxX13J2MC1xnFnuc@4_ zlwQ5IsJZ9Vbl?(dZwA$#IS6-6DKtifb9AA`Fb!Z<0Zg&y>@dJ)J;2;pr!LsI2N$_T zSPj4VVux;+T0)35KO2{W57=0%)zn*OHU6o!3!1xXYAT_Pah%Qv`68cv^3JgySF1h5 z#AF{!#NBq=np_})QmZ*dL7Pvq0S6Jf(R zLIv|_s`s8O71VsgJiaNzbA%mP_8G|e@jvSTGGk*Z9GFbUDvkYj`;FgU$baVQ63JK;a-eW$0F4cnY zvrwp&TohWJ?~t#s4nc8luvYMcFqk7m!@A>Mg}VqPbQ8w$XUtHN0s$0{w90)5cQzsb zgmv#U5{zBo^H7NeBSg>Ha2=GxniZ~V9$p1a1fgekU2_vG6DW}PGi+YnX3s2UBq^82 z$bChH2)BK2`bQ2F}CCL(2N1WtD7HKvn z#YDnlEq?g67lpPkWjVi{5sS8BZ%_v>uCa+HltXY_^M(8!u{mQNRD7tCddLo#GY_7^ zHZzOh-H}@}2bBdI#{MIRiJl#um~wp^IWa}+hS}_2=XVsI^Vmsa5moqueN1Q^=Fc_7 zABQzRp0EUbnmU@zR0r&?ThR{1TwW@>WqrlxQrMDR6QS2X6KgPq9mi=l!Q2e zl1K7rDl!kfVW6cM{sLqZs9hsTYZUqIiKal!i0$0ao-hDJiKc`|7)L!sJp&=P5UGOX zf3o(5sVa=pynrYK${kRpxDbax6}sty5b015X->tJFx6B_F3pHQPYh#6?opdm5fBP7 z(5GMkl#=R~{L(oAQ<05OpYBV5<82nf8IVpTXjhB?VhRP3Zf3{*ZkhYVJm327Xv%L^w5E#c>_u@=ym}qhA-rM1&(@V0jHE%#{U+G(NR|L2O&>HDrJZ@f)WZJDI-~CXCrpm z+{&={2fFBI2ic2`Dc9IvNrVINFw%C+&JC4lD2z-=Vx-6%v8s{X+R~X?O7;z7Hnfz{ z4eY(4U^G4^KTx=tSYbsdFMF6><8m%2ChaXEB%^G$S_^p^EEGFAx)%XXF%*W1SFr?( z(#6ASsPfrgpoY+TW@&;RGRGqx9k0D94<2$fOOkKW-t0~P)hVG^nop=Uno&NHD3-xQ zUg@;HvR`TpBUs7)m?EAf-lWN0pJ>PU(5-|B5%G*p_6XdF=UKFF=&i)U|zV(n0rpb%Zc(35)Ii%*5`n4$ z4(`0(w!PMR0XEiw1;T-I(I_i|@ySa%NCt5pL z8;G3Yj2t=6MRD>7|BEe56qJ!81WkpR? zeWIH~Sdk-G2NgtAO?QaOa3=}q6#{znNs>_TL7}%~1`zh7@@{khR2?Ulg*P^^Lg3Zg zZ)AXR=%dw)aHPQnaK5t5s`pL>Fo9z;d15j_lxl^r4@7`GQBYb$mW2@zPx5kBb+Xs3`)fx(K{OHHXwHAt^E2p~%jdI*gQlr`jdB;UB}5!|A4 z1EFx3wxUK%S0JSkwIX&j4D6@lplA_jh7m|~6JXN&q8i2x%rvWC1z2WN6inx97^3#dft(p#HiT&)jmKL_L?CcrIyXE;rz!I{iiRLIr}!`xkov1}>eTVQUT8X! zgfnpvNfO)yMC6QagKkI7yA(vxymH6P{_b8S0Xk7U&5GEAR$kQzgX;cg4c!9zp{rl4K$ZMWsiY3wr5S zxm${Wb4k`KrJP<2P4dyNb_@l z!h#gX?C{JpSDZD&Cz{6-$+^gpw?niTo zkQ38TlVlm9fX7Rk1@xubDTN-XsRL79t+ww7M#i94?B^(@IT--Ck(C2u?}Gb?O9h_R zLb^eVAP5TuU{QG>UJ*&;?TmkE1m40F6MA}#DM&Z!$h^2faTFLH1{B`B{6gW(`5p8_dqFf&u&5#k zWTx^eh*D&RUQuBMqbY6r#*KaDmtW4snXK{mL;Itr;GSo^I!N=Vd90Q{` zE9>=$%O~hh72A%4QK&i!KxzkM019<-INYu|yw)h;!RQzr7?1VjC^d}xz_EP)gVd!x z#b8730MRpx84z~b&PI`Fq5;y2SX!rMhvM5fFu}CAFLN}2Jy_Ku9CWpUQ*gJs_1P9jM7hG(F%M}cLO7{s!mLUrJj!X zr?=pVLG$S2sM=s?D1DKkPO3_s!1rh(WCuMHtph1Ucfji)A2d&r6-@+&u0TwPMG+Nx zX;9D;7epz-5zZ8GG|141i#v-{s4r{``>L8ihRAs15(EKCTY^)WXp)x_3PMEcOHrba z(^%%)RV5b@G?XvSE(8|Q_loE`eK)!xhYC6|uZj-SQPCA7%uHBI83ymowIUfpn@(bL zWDJR%CkPddXg}3|jKsHmdKgx*;xR?HK&n_fjh5uF+#20Tb0T$uZbDufOQ4c?$x*Hh zI^i4$5zZhjXOJ2(6a(@=t-V3|rbX!>aAP8Y%7<0j%#nJd-mAki@E4S)*7e8VobFD4 zJ?TsL`HlMe^}D`lA79H8HFf(|o?za;t-t#1-QE4u+7fM+xrjCr~dqK`SFC5JdNv{37}u(1@QbKpY79fw?U!}{cI5L7WE}0MtCubSb7QR z8`pKz)5Gz}Z%IVds-~Hvg{{yvjtrY+O literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/Contents.json new file mode 100644 index 00000000000..4d4ea0cecf1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gifting.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/gifting.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Gift.imageset/gifting.pdf new file mode 100644 index 0000000000000000000000000000000000000000..704018f2118506e9d88e1b16f535bee10cce58fc GIT binary patch literal 8960 zcmeHNS#KN35q_Uv(Juye18CUy0W1uxPXvjvij;>1f;`aFSmK5v6_N_PyV(Dpuey6` zni6HnK@I~k4cI;3^ijuG)!j9IcKYhgiRwewDIvw?{ZE|`-+d=$e;n4A`zxUVo&myd zb~ayM4&pCD_A>v38XHeR{#s{wwYi=z*GH!ryS%wwFZRpL`p-Y#48mgE^XJ`eza8e+ z-LC@6YiW|!V)(eY!c&O!I6hfpos#A0;`U||_Hr+NE%43e=1Ak9^X~RNjWjM8C9ZE* z)1vc>izAa${}b6On><7*1FBpP50JfcLAg--SIN2mZ({jDkW_5u$5eo2O!CeY0cG!v z46@on_9-dvLwT3yCOiDr4eN_)TeQgk9r?Tv!I(hrGin%Z=wZJ^dRXOr^rI16@HVLN zDJ3sMEKdg>f6TTAoeMp*N=BN|du37f-o5@rkEjE@Q|M9UdUyZ@73e2x|0+56XL^7} z##;+F}8S z;?^jtL!YAKg=|6bf z=U0pCZcGmX^H7BD7VD1RW521BTsNTWqjxD8VR~gOPCH@}I*bD*Xwgm6Oz5i9JkNwS ztz&Ol9L&iU<0UaAgOg}H&k?N{T68=MW2=rQVT>F1U-1Ns##Aius=MkwbnEU<|3+u& zug+qB{BNBV!TM;iC8^X~C8e_4o3Mr`xZ#tWuGdM%M9SM}ee!~h3e`Qy z(jqXnPAZSPXZjT~fKE`!H&$*y*sF{_!0H;I$5l?k8!ShFnS%Bt8A}^`Z)DgS|m@J~t3E0?phdEp?1FC8k zh{0~G<%BFc;ukL76c)7?GL;G;C7MYh{OwKK#;TK zU^)m7pE5%ECTKW;g1qWS7i1(xO*0is!J!VOBaJ76QY5-SGE}=0wGoJob6|5MiE!XU z5ZWe}{aRhk8xYu1_|PCs8^)MjKoDu5KU0vZfDRUInTqIwZ{Y_fG^&FjY|iqP^&obX z>R@xA#CRmZedYh>S>%y~`46yQya&eGQop;{q!*q2xNGR6$zAha51FwkGq$3g5D<1y z!XPs(gRBg)=+e(1DYO_RI4F0}59f`Lh$Ps2VvyJ-YUa#Qps_E*v=c;mqPlGY8|@nL zuCN&;n>~z$p$8Vep0K7T)JLA6+;bPiX~7lsLbjsvy;8`Mrs`bu0|=huNL3605dAb{ zhYkcGmx5)0D8B`QEb>#%mKn4rp9sN+&`NL{KguJfrv6-WWv*Cesj0b9^>f6QB*+ zMCgQah)&CJL{9QBgdk<_F$fvi`2aNzo{HiEU?B-W;}mi=ph_hkOi-a5Ls$d}580N( z9;ZpncL3HHzwfMkz589h-yQifLv`m;w#IEaaosvFJe8r3)~(}I|C#+a>(*a#-GcM| z{l?c4#<&;$hi=73C{7fu6!(!Cj+;V7iQ$#oxd{BD67<>TY^sf+DE{_*Ki?1e{p(UI zK7Tjg{VfCKvkpBzr5Y_}Zv}XsVabImvYC7VlJmcu7Y4z#bF*)k{^?I|%*P;4R zf{+!M)haHf1uIZy9>7IK`i;~b~=*#(jzS>+K4*s|uK6Kb( zTvtw?^tZsh42KWk^+#IKPoT0|eh!~sbE|>yh4~OtINIEU@ZS^;d*OP8K%yVd6%W++^;}VwLIwPr-R64u uR(!L(__p|KcDC8TS>%iC|JnKSmx1gGPwn>e?LJ#NqTbX!d-nQ=H~$3H|2)e8 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json new file mode 100644 index 00000000000..7d404648910 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf new file mode 100644 index 0000000000000000000000000000000000000000..92b05022c25189e8165da1acc55c538e57b452ff GIT binary patch literal 9372 zcmbuFTW@7Y5ryB+uQ)G}0Ey$i_ihPAB3B_mgc%-?5D$)XGGp*1*dCOC`0sqHx=(w1 z9|wV$NE6SRzE)SQT2*^K`s%YUe(n!-xAS|qzyJC#yZ!zbzt~^=;fL>z`-?yR?(4rC zub#m8b@7|y-Mgpv`@q0A1N!pe{=;{x3HImycQ&U_e9L+dmv@ifT|OM|u6}s&_aA>e zmU{c)>nGdvi~EP$fxS6C-hX(wIzDQPIr5WF?!VvvY3~lsem(!-;|cQN^wXbNikXj3 z568>f-Nk3ezh7S+-+cMW{?oVXKV3cU{AtQu_~-Md`FH*hqjPoH^LI6P-~9gSc9##) z=Mu_(Ipi1%;0p8Z;9GFD>~Ee20_I#|aQitIsKzCwAZS@~Dtn+MN0V*jMXH#1&OKmo_=H{HYana>a8Mic6>sz?oO5T@)&x|vOvvCrp zwnGag$7Np+F2qzEm*4=d_$4n)S(w@E06sL%Cn!c=I>a5a1Jd?OW;v$%T57C-rNxFJ zUqEc(vO%Y*IWK_?`qa}FX-R?muMd*2@RIi9yp8n(>jznGT&(WHp%#~u?lA5zEPJ24 zEHaD~uzp$c%9cW0cCyDbIap0?(Y*04;gCE&AY?or2u7~X<_!;Fzq&NeFyCFL@~*^Z zBp!Uo(be_5bf}AY*3FJ37GGP~Ps}sH7q{#qw{Z>^a-3>uu5rIou{3VFbBDk+xGLw$ zq44>k>O(!l6>Z^Kw_-5&vUIrTPp_6niX-m;_)_*=W#;9;&T*Imw;Wab{0Od zau1n?|7&^2O>M)RmWm5$Vqpqn7A_HFWDZe$Ae<66W!c6koXf8RY?^O|+|}+qGq;>D zk;kjM-M2s6-R*YAyKe>^@RKNSCrazw=edRk7#t@oN*wgZB_{oS@!{_3>H7Z8*fI^$ z`t1Jp^7?K~P?J4GM*O;Xb9wjf_@g79>FS^C_3mk}|NV>KJ-=Und%Sr(?*H}5Ep)Pe zi%{nsc&rtzfByZ`mv5ggpN_`apL_e?+uvS3{&hv-`eQ$+o%Y9nyZDkLO>6x4IQyFm zvcNBIzI%W9-!1)zH+z|XB%1%U_9-vCI%XuYdG8b|iGql#huWIYL5$py zRYaYye?yc_bP~`lBs*qL?B*HE2OmocV&&{IClPDbR7rpEH2I47hkrxzmUK9+DUq zV|vNhHQDqnb@u67V!V|ixAaKo&}vUXdSAFV*xuDC5n4LijvMdP zb;L|Wx4U#e6)Z~G2WYp2#b$34mS&%){#!7-XlY8{qzh6xv(LwAw$l13hmZ7syIar; z*=NsmM8e{A^j8M1c3&lO?1mhQC=Yx65DT(oZoeA1-F`?2=6muG|4F6z2-VB*>;1<_ zEr2g=HJF?{Ik}Bw2KkTdXp(L_u+JYa!umsq>FIEJ=i(lH8cM1dQVX1o)B-c0W;m{b zOou*c%6Mp~6(adnaL!(Z;|kM70|z148X-|~ZfMuno|)B$@#~>A#82kzsKQbM?|^BM zm43L>c17K1t_cOf1*ihq!$xKTp+>jZdyFL;4qBUo9Qqr*_VB|O=YlWHHbvCRY%5-v zR(lPbb5}-NVJill>~o7Ob4`-)nWGR~wIy`CbWG(osv5SugR(^?Hc2giSQ5b{ul5Hd zS4BD!x-o>7FcF;SAgTzTy`;d2tSE3EOh%z<$tX&RtV-no5sQS9hg%)%Wn>q80wV-R zd~jT1TpAuP^{(;K_-IhQfH-4`grZcurG)~fvOozn=lq;pCK1ncvpQKjEM+R$5}PLq zCPuI%N{d7##bqlxSt>Gl^sY(}j7dRZ(y~>aU!pmaDiD%Xv2ba_U*ch*YC_Bk!m>-? zBIsOCc_M3XZ6OQ2@-Rm81*hcxQL2_gte1UoKoq1Evq8!Amq8^LHx5`cjIPrlkj0d2 zQ9aBn+K(~ALxL!u`s-5q@pHeM?Af$M^6X{1a?5UOl39*HQ&U|QO9NS+q4EkaVm}PZ zqr%Ue94I8BNUN2AOW8rT*-M&RZ>f3=il=mt6Y9YHC(m%DRWP2XBHx9Ch0DrkU5tb? zKSZhnGOA3BizU$<7w|3W2V_U8hO|Jw6_OxRt(3__9Lt^tYT(eyO)g*{76?>O6uneu z-lZyJ6D)S^ZB*Htog0-oJR3{a#kUgzJQQGM`6SoaRZ}(LFMXbXjCCw zyCfNY10u^%E%T1u5g3Id2!jUte$b)LnUHP#pp}+j7RCQn!J7j`s!BVR=Mu7Qj7`VW zIdNQD2}|L4AUdcDPDUl3Px~=ePay>9snWG199@38r6Ky zhyn%J6rb?$D5pbFReV9~R*9N(F0kdcchXhQGq|!koFSE`dyO6!grHqPJnpK5qGvTO zROnh}0poD^2DeAs5&j@j6_%qY9K2d%#8yPXF@^vkr75#GD3*2x7_kkB3L!b^1T;Em zeGP=a>Ih-c=ixdXLrSaT;{vf`_NRw|Lx8~YIJ4IFW%P0!4@YVemgLopg3bvQAq~1P zgtzMFge-dR2%c6zF)H)8HVR}%8ZCRD4lZ4lLQn7ZXqO9faxqWY~=F1mlAs^=m8^Zh;fu;j9$C~1G|%E z8P$*>YCfVC6s^k75`zkrq1^23QkO3j-uvAYUsF`wK4UOgA&Y8CTM|J|4AONfL_@On zPOvzo6^i#>QP4On0cXMtX;IPup=E0F#r3w+dC{Y;;L!3!5jhp#l682#6y-@|YVwV& zgQzovTDs8b!a9L2n3#xEK*x$RfJO4HPTG`ytaTGK1joqyssvfR$eg(W?@uIfr_Y4H4^Wj z8?0(fZ;+?JvZ(M)NkFyjB!o;%4w(o-WTJl6b~iEEP=e&0{u$YI$q7cy0}~@&B}>H_@^7L< za+c9e2wBy9b4pxcGWg_luN#ZM2;H>#ju5_7Pk mapToSignal { sharedContext in + sharedContext.sharedContext.presentationData + } + + if #available(iOS 13.0, *) { + let darkAppearanceSignal = presentationDataSignal + |> map { + $0.theme.overallDarkAppearance + } + |> distinctUntilChanged + |> deliverOnMainQueue + + let _ = darkAppearanceSignal.start(next: { isDark in + UIApplication.findKeyWindow()?.overrideUserInterfaceStyle = isDark ? .dark : .light + }) + } // self.authContext.set(self.sharedContextPromise.get() @@ -1286,6 +1323,10 @@ private class UserInterfaceStyleObserverWindow: UIWindow { // MARK: Nicegram Onboarding (we put the telegram start navigation code in a closure that we will call when we are done with processing our onboarding) let onNicegramOnboardingComplete = { // + let _ = presentationDataSignal.start(next: { presentationData in + ng_setTgLangCode(presentationData.strings.baseLanguageCode) + }) + let contextReadyDisposable = MetaDisposable() let startTime = CFAbsoluteTimeGetCurrent() @@ -1496,11 +1537,12 @@ private class UserInterfaceStyleObserverWindow: UIWindow { } else { AppCache.wasOnboardingShown = true if let rootController = window.rootViewController { - let langCode = Locale.currentAppLocale.languageWithScriptCode - let controller = onboardingController(languageCode: langCode, onComplete: { [weak rootController] in - rootController?.dismiss(animated: true) - onNicegramOnboardingComplete() - }) + let controller = onboardingController( + onComplete: { [weak rootController] in + rootController?.dismiss(animated: true) + onNicegramOnboardingComplete() + } + ) controller.modalPresentationStyle = .fullScreen rootController.present(controller, animated: false) @@ -2079,9 +2121,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { |> deliverOnMainQueue).start(next: { context in if let context = context { Queue().async { - let presentationData = context.context.sharedContext.currentPresentationData.with({ $0 }) self.fetchNGUserSettings(context.context.account.peerId.id._internalGetInt64Value()) - self.fetchLocale(lang: presentationData.strings.baseLanguageCode) } } }) @@ -2299,7 +2339,7 @@ private class UserInterfaceStyleObserverWindow: UIWindow { stableId: callUpdate.callId, handle: "\(callUpdate.peer.id.id._internalGetInt64Value())", phoneNumber: phoneNumber.flatMap(formatPhoneNumber), - isVideo: false, + isVideo: callUpdate.isVideo, displayTitle: callUpdate.peer.debugDisplayTitle, completion: { error in if let error = error { @@ -3006,12 +3046,6 @@ private class UserInterfaceStyleObserverWindow: UIWindow { updateGlobalNGSettings() } - private func fetchLocale(lang: String) { - #if !targetEnvironment(simulator) - downloadLocale(lang) - #endif - } - private func maybeCheckForUpdates() { #if targetEnvironment(simulator) #else diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index a1aa16eac13..3426f8ca31d 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -313,7 +313,7 @@ final class AuthorizedApplicationContext { let chatLocation: NavigateToChatControllerParams.Location if let _ = threadData, let threadId = firstMessage.threadId { chatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: firstMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + messageId: MessageId(peerId: firstMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false ).normalized) } else { guard let peer = firstMessage.peers[firstMessage.id.peerId] else { @@ -905,7 +905,7 @@ final class AuthorizedApplicationContext { let chatLocation: NavigateToChatControllerParams.Location if let threadId = threadId { chatLocation = .replyThread(ChatReplyThreadMessage( - messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false + messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )) } else { chatLocation = .peer(peer) diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index c009a047424..c5dc13c766e 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -246,6 +246,9 @@ func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentati }, send: { message in let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) + |> `catch` { _ in + return .single(.result([])) + } |> mapToSignal { result -> Signal<[Message], NoError> in guard case let .result(result) = result else { return .complete() diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 014e6ac8dff..f33d0ca9d4e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -108,6 +108,9 @@ extension ChatControllerImpl { let _ = (combineLatest( self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) + |> `catch` { _ in + return .single(.result([])) + } |> mapToSignal { result -> Signal<[Message], NoError> in guard case let .result(result) = result else { return .complete() @@ -127,7 +130,7 @@ extension ChatControllerImpl { let navigateToLocation: NavigateToChatControllerParams.Location if let message = messages.first, let threadId = message.threadId, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) { - navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } else { navigateToLocation = .peer(peer) } @@ -145,7 +148,7 @@ extension ChatControllerImpl { if let navigationController = self.effectiveNavigationController { var chatLocation: NavigateToChatControllerParams.Location = .peer(peer) if case let .channel(channel) = peer, channel.flags.contains(.isForum), let message = message, let threadId = message.threadId { - chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } var quote: ChatControllerSubject.MessageHighlight.Quote? diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 2964864ea04..c82be4bbb9a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -844,7 +844,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD return } - if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first { + if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil, forPeerId: selfController.chatLocation.peerId), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first { if let webpage = webpageCache[detectedUrl] { progress?.set(.single(false)) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index d5ca6f582d6..29e763d7c7a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -53,6 +53,18 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() } + // MARK: Nicegram TranslateEnteredMessage + let peerId = selfController.presentationInterfaceState.chatLocation.peerId + let isSecretChat = (peerId?.namespace == Namespaces.Peer.SecretChat) + + let inputText = selfController.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string + let isInputTextEmpty = inputText + .trimmingCharacters(in: .whitespacesAndNewlines) + .isEmpty + + let canTranslate = !isSecretChat && !isInputTextEmpty + // + let controller = ChatSendMessageActionSheetController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, peerId: selfController.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputView, canSendWhenOnline: sendWhenOnlineAvailable, completion: { [weak selfController] in guard let selfController else { return @@ -78,7 +90,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no selfController.openScheduledMessages() } } - }, /* MARK: Nicegram TranslateEnteredMessage (translate + chooseLanguage) */ translate: { [weak selfController] in + }, /* MARK: Nicegram TranslateEnteredMessage (canTranslate, translate, chooseLanguage) */ canTranslate: canTranslate, translate: { [weak selfController] in guard let selfController else { return } let chatId = selfController.chatLocation.peerId let textToTranslate = selfController.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 9f6beaff476..6ecc648b379 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -220,7 +220,7 @@ func updateChatPresentationInterfaceStateImpl( } } - if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0) { + if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { selfController.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? @@ -301,7 +301,7 @@ func updateChatPresentationInterfaceStateImpl( let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText - if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0) { + if let (updatedEditingUrlPreviewState, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: selfController.context, currentQuery: selfController.editingUrlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { selfController.editingUrlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? @@ -452,7 +452,7 @@ func updateChatPresentationInterfaceStateImpl( selfController.leftNavigationButton = nil } - if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { + if let button = rightNavigationButtonForChatInterfaceState(context: selfController.context, presentationInterfaceState: updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: selfController.rightNavigationButton, target: selfController, selector: #selector(selfController.rightNavigationButtonAction), chatInfoNavigationButton: selfController.chatInfoNavigationButton, moreInfoNavigationButton: selfController.moreInfoNavigationButton) { if selfController.rightNavigationButton != button { var animated = transition.isAnimated if let currentButton = selfController.rightNavigationButton?.action, currentButton == button.action { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f19efbab3ab..d507ff95ddd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -131,6 +131,9 @@ import ChatNavigationButton import WebsiteType import ChatQrCodeScreen import PeerInfoScreen +import MediaEditorScreen +import WallpaperGalleryScreen +import WallpaperGridScreen public enum ChatControllerPeekActions { case standard @@ -763,7 +766,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .full: break } - } else if let _ = media as? TelegramMediaGiveaway { + } else if media is TelegramMediaGiveaway || media is TelegramMediaGiveawayResults { let progress = params.progress let presentationData = strongSelf.presentationData @@ -817,7 +820,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } switch action.action { - case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults: + case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil))) @@ -939,6 +942,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.presentThemeSelection() return true } + if peer is TelegramChannel { + return true + } strongSelf.chatDisplayNode.dismissInput() var options = WallpaperPresentationOptions() var intensity: Int32? @@ -978,10 +984,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.dismissInput() let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId - let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration)) + let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil)) strongSelf.push(controller) return true - case let .giftCode(slug, _, _, _, _): + case let .giftCode(slug, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true case let .suggestedProfilePhoto(image): @@ -2432,12 +2438,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first { let chatPresentationInterfaceState = strongSelf.presentationInterfaceState var warnAboutPrivate = false + var canShareToStory = false if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel { + if case .broadcast = channel.info { + canShareToStory = true + } if channel.addressName == nil { warnAboutPrivate = true } } let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), updatedPresentationData: strongSelf.updatedPresentationData, shareAsLink: true) + + if let message = messages.first, message.media.contains(where: { media in + if media is TelegramMediaContact || media is TelegramMediaPoll { + return true + } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker { + return true + } else { + return false + } + }) { + canShareToStory = false + } + if message.text.containsOnlyEmoji { + canShareToStory = false + } + + if canShareToStory { + shareController.shareStory = { [weak self] in + guard let self else { + return + } + Queue.mainQueue().after(0.15) { + self.openStorySharing(messages: messages) + } + } + } shareController.openShareAsImage = { [weak self] messages in if let strongSelf = self { strongSelf.present(ChatQrCodeScreen(context: strongSelf.context, subject: .messages(messages)), in: .window(.root)) @@ -3128,9 +3164,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] controller, f in if let strongSelf = self { - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds([id]) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [id], type: .forLocalPeer).startStandalone() } f(.dismissWithoutContent) @@ -4164,23 +4197,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } - }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in + }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in guard let self else { return } let botName = self.presentationInterfaceState.renderedPeer?.peer.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" let context = self.context let peerId = self.chatLocation.peerId - var createNewGroupImpl: (() -> Void)? - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [peerType], hasContactSelector: false, createNewGroup: { - createNewGroupImpl?() - }, hasCreation: true)) let presentConfirmation: (String, Bool, @escaping () -> Void) -> Void = { [weak self] peerName, isChannel, completion in guard let strongSelf = self else { return } - + var attributedTitle: NSAttributedString? let attributedText: NSAttributedString @@ -4230,62 +4259,109 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - + let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationSend, action: { - completion() })]) strongSelf.present(controller, in: .window(.root)) } - controller.peerSelected = { [weak self, weak controller] peer, _ in - guard let strongSelf = self else { - return + if case .user = peerType, maxQuantity > 1 { + let presentationData = self.presentationData + var reachedLimitImpl: ((Int32) -> Void)? + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection, options: [], isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil { + return true + } else { + return false + } + }, limit: maxQuantity, reachedLimit: { limit in + reachedLimitImpl?(limit) + })) + controller.navigationPresentation = .modal + reachedLimitImpl = { [weak controller] limit in + guard let controller else { + return + } + HapticFeedback().error() + controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.RequestPeer_ReachedMaximum(limit), timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) } - if case .user = peerType { - let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peer.id).startStandalone() - controller?.dismiss() - } else { - var isChannel = false - if case let .channel(channel) = peer, case .broadcast = channel.info { - isChannel = true + + let _ = (controller.result + |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in + guard let controller else { + return + } + var peerIds: [PeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }) + } + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: peerIds).startStandalone() + controller.dismiss() + }) + + self.push(controller) + } else { + var createNewGroupImpl: (() -> Void)? + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [peerType], hasContactSelector: false, createNewGroup: { + createNewGroupImpl?() + }, hasCreation: true)) + + controller.peerSelected = { [weak self, weak controller] peer, _ in + guard let strongSelf = self else { + return } - let peerName = peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) - presentConfirmation(peerName, isChannel, { - let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peer.id).startStandalone() + if case .user = peerType { + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peer.id]).startStandalone() controller?.dismiss() - }) + } else { + var isChannel = false + if case let .channel(channel) = peer, case .broadcast = channel.info { + isChannel = true + } + let peerName = peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) + presentConfirmation(peerName, isChannel, { + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peer.id]).startStandalone() + controller?.dismiss() + }) + } } - } - createNewGroupImpl = { [weak controller] in - switch peerType { - case .user: - break - case let .group(group): - let createGroupController = createGroupControllerImpl(context: context, peerIds: group.botParticipant || group.botAdminRights != nil ? (peerId.flatMap { [$0] } ?? []) : [], mode: .requestPeer(group), willComplete: { peerName, complete in - presentConfirmation(peerName, false, { - complete() + createNewGroupImpl = { [weak controller] in + switch peerType { + case .user: + break + case let .group(group): + let createGroupController = createGroupControllerImpl(context: context, peerIds: group.botParticipant || group.botAdminRights != nil ? (peerId.flatMap { [$0] } ?? []) : [], mode: .requestPeer(group), willComplete: { peerName, complete in + presentConfirmation(peerName, false, { + complete() + }) + }, completion: { peerId, dismiss in + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peerId]).startStandalone() + dismiss() }) - }, completion: { peerId, dismiss in - let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).startStandalone() - dismiss() - }) - createGroupController.navigationPresentation = .modal - controller?.replace(with: createGroupController) - case let .channel(channel): - let createChannelController = createChannelController(context: context, mode: .requestPeer(channel), willComplete: { peerName, complete in - presentConfirmation(peerName, true, { - complete() + createGroupController.navigationPresentation = .modal + controller?.replace(with: createGroupController) + case let .channel(channel): + let createChannelController = createChannelController(context: context, mode: .requestPeer(channel), willComplete: { peerName, complete in + presentConfirmation(peerName, true, { + complete() + }) + }, completion: { peerId, dismiss in + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerIds: [peerId]).startStandalone() + dismiss() }) - }, completion: { peerId, dismiss in - let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).startStandalone() - dismiss() - }) - createChannelController.navigationPresentation = .modal - controller?.replace(with: createChannelController) + createChannelController.navigationPresentation = .modal + controller?.replace(with: createChannelController) + } } + self.push(controller) } - self.push(controller) }, saveMediaToFiles: { [weak self] messageId in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).startStandalone(next: { message in @@ -5215,7 +5291,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .pinnedMessages = presentationInterfaceState.subject { strongSelf.chatTitleView?.titleContent = .custom(presentationInterfaceState.strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) } else { - strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) + strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) let imageOverride: AvatarNodeImageOverride? if strongSelf.context.account.peerId == peer.id { imageOverride = .savedMessagesIcon @@ -5661,11 +5737,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .peer: replyThreadType = .replies case let .replyThread(replyThreadMessage): - replyThreadId = Int64(replyThreadMessage.messageId.id) - if replyThreadMessage.isChannelPost { - replyThreadType = .comments - } else { + if replyThreadMessage.messageId.peerId == context.account.peerId { + replyThreadId = replyThreadMessage.threadId replyThreadType = .replies + } else { + replyThreadId = replyThreadMessage.threadId + if replyThreadMessage.isChannelPost { + replyThreadType = .comments + } else { + replyThreadType = .replies + } } case .feed: replyThreadType = .replies @@ -5701,6 +5782,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + let savedMessagesPeerId: PeerId? + if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.messageId.peerId == context.account.peerId { + savedMessagesPeerId = PeerId(makeMessageThreadId(replyThreadMessage.messageId)) + } else { + savedMessagesPeerId = nil + } + + let savedMessagesPeer: Signal<(peer: EnginePeer?, messageCount: Int)?, NoError> + if let savedMessagesPeerId { + let threadPeerId = savedMessagesPeerId + let basicPeerKey: PostboxViewKey = .basicPeer(threadPeerId) + let countViewKey: PostboxViewKey = .historyTagSummaryView(tag: MessageTags(), peerId: peerId, threadId: savedMessagesPeerId.toInt64(), namespace: Namespaces.Message.Cloud) + savedMessagesPeer = context.account.postbox.combinedView(keys: [basicPeerKey, countViewKey]) + |> map { views -> (peer: EnginePeer?, messageCount: Int)? in + let peer = ((views.views[basicPeerKey] as? BasicPeerView)?.peer).flatMap(EnginePeer.init) + + var messageCount = 0 + if let summaryView = views.views[countViewKey] as? MessageHistoryTagSummaryView, let count = summaryView.count { + messageCount += Int(count) + } + + return (peer, messageCount) + } + } else { + savedMessagesPeer = .single(nil) + } + var isScheduledOrPinnedMessages = false switch subject { case .scheduledMessages, .pinnedMessages, .messageOptions: @@ -5763,103 +5871,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView, messageAndTopic, + savedMessagesPeer, onlineMemberCount, hasScheduledMessages ) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, onlineMemberCount, hasScheduledMessages in + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView, messageAndTopic, savedMessagesPeer, onlineMemberCount, hasScheduledMessages in if let strongSelf = self { strongSelf.hasScheduledMessages = hasScheduledMessages - let message = messageAndTopic.message - - var count = 0 - if let message = message { - for attribute in message.attributes { - if let attribute = attribute as? ReplyThreadMessageAttribute { - count = Int(attribute.count) - break - } - } - } - - var peerIsMuted = false - if let threadData = messageAndTopic.threadData { - if case let .muted(until) = threadData.notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - peerIsMuted = true - } - } else if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - peerIsMuted = true - } - } - - if let threadInfo = messageAndTopic.threadData?.info { - strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) - - let avatarContent: EmojiStatusComponent.Content - if strongSelf.chatLocation.threadId == 1 { - avatarContent = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(strongSelf.presentationData.theme)) - } else if let fileId = threadInfo.icon { - avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: strongSelf.presentationData.theme.list.itemAccentColor, loopMode: .count(1)) - } else { - avatarContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: 32.0, height: 32.0)) - } - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent) - } else { - strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count) - } - - var wasGroupChannel: Bool? - if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - wasGroupChannel = true - } else { - wasGroupChannel = false - } - } - var isGroupChannel: Bool? - if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { - if case .group = info { - isGroupChannel = true - } else { - isGroupChannel = false - } - } - let firstTime = strongSelf.peerView == nil - - if wasGroupChannel != isGroupChannel { - if let isGroupChannel = isGroupChannel, isGroupChannel { - let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) - let disposable = DisposableSet() - disposable.add(recentDisposable) - disposable.add(adminsDisposable) - strongSelf.chatAdditionalDataDisposable.set(disposable) - } else { - strongSelf.chatAdditionalDataDisposable.set(nil) - } - } - - strongSelf.peerView = peerView - strongSelf.threadInfo = messageAndTopic.threadData?.info - - if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle - } - - var peerDiscussionId: PeerId? - var peerGeoLocation: PeerGeoLocation? - var currentSendAsPeerId: PeerId? - if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { - currentSendAsPeerId = cachedData.sendAsPeerId - if case .broadcast = peer.info { - if case let .known(value) = cachedData.linkedDiscussionPeerId { - peerDiscussionId = value - } - } else { - peerGeoLocation = cachedData.peerGeoLocation - } - } var renderedPeer: RenderedPeer? var contactStatus: ChatContactStatus? var copyProtectionEnabled: Bool = false @@ -5898,131 +5917,251 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media) } - var isNotAccessible: Bool = false - if let cachedChannelData = peerView.cachedData as? CachedChannelData { - isNotAccessible = cachedChannelData.isNotAccessible - } - - if firstTime && isNotAccessible { - strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId) - } - - var hasBots: Bool = false - if let peer = peerView.peers[peerView.peerId] { - if let cachedGroupData = peerView.cachedData as? CachedGroupData { - if !cachedGroupData.botInfos.isEmpty { - hasBots = true + if let savedMessagesPeerId { + let mappedPeerData = ChatTitleContent.PeerData( + peerId: savedMessagesPeerId, + peer: savedMessagesPeer?.peer?._asPeer(), + isContact: false, + notificationSettings: nil, + peerPresences: [:], + cachedData: nil + ) + strongSelf.chatTitleView?.titleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: nil, isScheduledMessages: false, isMuted: false, customMessageCount: savedMessagesPeer?.messageCount ?? 0, isEnabled: true) + + strongSelf.peerView = peerView + + if strongSelf.isNodeLoaded { + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle + } + + let animated = false + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { + return $0.updatedPeer { _ in + return renderedPeer + }.updatedSavedMessagesTopicPeer(savedMessagesPeer?.peer) + }) + + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: savedMessagesPeer?.peer) + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false + strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile + } else { + let message = messageAndTopic.message + + var count = 0 + if let message = message { + for attribute in message.attributes { + if let attribute = attribute as? ReplyThreadMessageAttribute { + count = Int(attribute.count) + break + } } - } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let channel = peer as? TelegramChannel, case .group = channel.info { - if !cachedChannelData.botInfos.isEmpty { - hasBots = true + } + + var peerIsMuted = false + if let threadData = messageAndTopic.threadData { + if case let .muted(until) = threadData.notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + peerIsMuted = true + } + } else if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + peerIsMuted = true } } - } - - let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive - - var explicitelyCanPinMessages: Bool = false - if let cachedUserData = peerView.cachedData as? CachedUserData { - explicitelyCanPinMessages = cachedUserData.canPinMessages - } else if peerView.peerId == context.account.peerId { - explicitelyCanPinMessages = true - } - - var animated = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { - animated = true - } - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel { - if peer.participationStatus != updated.participationStatus { + + if let threadInfo = messageAndTopic.threadData?.info { + strongSelf.chatTitleView?.titleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) + + let avatarContent: EmojiStatusComponent.Content + if strongSelf.chatLocation.threadId == 1 { + avatarContent = .image(image: PresentationResourcesChat.chatGeneralThreadIcon(strongSelf.presentationData.theme)) + } else if let fileId = threadInfo.icon { + avatarContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: strongSelf.presentationData.theme.list.mediaPlaceholderColor, themeColor: strongSelf.presentationData.theme.list.itemAccentColor, loopMode: .count(1)) + } else { + avatarContent = .topic(title: String(threadInfo.title.prefix(1)), color: threadInfo.iconColor, size: CGSize(width: 32.0, height: 32.0)) + } + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setStatus(context: strongSelf.context, content: avatarContent) + } else { + strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count) + } + + var wasGroupChannel: Bool? + if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + wasGroupChannel = true + } else { + wasGroupChannel = false + } + } + var isGroupChannel: Bool? + if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + isGroupChannel = true + } else { + isGroupChannel = false + } + } + let firstTime = strongSelf.peerView == nil + + if wasGroupChannel != isGroupChannel { + if let isGroupChannel = isGroupChannel, isGroupChannel { + let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) + let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(engine: strongSelf.context.engine, postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in }) + let disposable = DisposableSet() + disposable.add(recentDisposable) + disposable.add(adminsDisposable) + strongSelf.chatAdditionalDataDisposable.set(disposable) + } else { + strongSelf.chatAdditionalDataDisposable.set(nil) + } + } + + strongSelf.peerView = peerView + strongSelf.threadInfo = messageAndTopic.threadData?.info + + if strongSelf.isNodeLoaded { + strongSelf.chatDisplayNode.overlayTitle = strongSelf.overlayTitle + } + + var peerDiscussionId: PeerId? + var peerGeoLocation: PeerGeoLocation? + var currentSendAsPeerId: PeerId? + if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData { + currentSendAsPeerId = cachedData.sendAsPeerId + if case .broadcast = peer.info { + if case let .known(value) = cachedData.linkedDiscussionPeerId { + peerDiscussionId = value + } + } else { + peerGeoLocation = cachedData.peerGeoLocation + } + } + + var isNotAccessible: Bool = false + if let cachedChannelData = peerView.cachedData as? CachedChannelData { + isNotAccessible = cachedChannelData.isNotAccessible + } + + if firstTime && isNotAccessible { + strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId) + } + + var hasBots: Bool = false + if let peer = peerView.peers[peerView.peerId] { + if let cachedGroupData = peerView.cachedData as? CachedGroupData { + if !cachedGroupData.botInfos.isEmpty { + hasBots = true + } + } else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let channel = peer as? TelegramChannel, case .group = channel.info { + if !cachedChannelData.botInfos.isEmpty { + hasBots = true + } + } + } + + let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive + + var explicitelyCanPinMessages: Bool = false + if let cachedUserData = peerView.cachedData as? CachedUserData { + explicitelyCanPinMessages = cachedUserData.canPinMessages + } else if peerView.peerId == context.account.peerId { + explicitelyCanPinMessages = true + } + + var animated = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState { animated = true } - } - - var didDisplayActionsPanel = false - if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - didDisplayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - didDisplayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - didDisplayActionsPanel = true + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel { + if peer.participationStatus != updated.participationStatus { + animated = true } } - } - - var displayActionsPanel = false - if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { - if !peerStatusSettings.flags.isEmpty { - if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.canShareContact) { - displayActionsPanel = true - } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { - displayActionsPanel = true - } else if peerStatusSettings.contains(.suggestAddMembers) { - displayActionsPanel = true + + var didDisplayActionsPanel = false + if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { + if !peerStatusSettings.flags.isEmpty { + if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { + didDisplayActionsPanel = true + } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { + didDisplayActionsPanel = true + } else if peerStatusSettings.contains(.canShareContact) { + didDisplayActionsPanel = true + } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { + didDisplayActionsPanel = true + } else if peerStatusSettings.contains(.suggestAddMembers) { + didDisplayActionsPanel = true + } } } - } - - if displayActionsPanel != didDisplayActionsPanel { - animated = true - } - - if strongSelf.preloadHistoryPeerId != peerDiscussionId { - strongSelf.preloadHistoryPeerId = peerDiscussionId - if let peerDiscussionId = peerDiscussionId { - strongSelf.preloadHistoryPeerIdDisposable.set(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId)) - } else { - strongSelf.preloadHistoryPeerIdDisposable.set(nil) + + var displayActionsPanel = false + if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings { + if !peerStatusSettings.flags.isEmpty { + if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.canShareContact) { + displayActionsPanel = true + } else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) { + displayActionsPanel = true + } else if peerStatusSettings.contains(.suggestAddMembers) { + displayActionsPanel = true + } + } } - } - - strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { - return $0.updatedPeer { _ in - return renderedPeer - }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId) - .updatedCopyProtectionEnabled(copyProtectionEnabled) - .updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - - if let channel = renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { + + if displayActionsPanel != didDisplayActionsPanel { + animated = true + } + + if strongSelf.preloadHistoryPeerId != peerDiscussionId { + strongSelf.preloadHistoryPeerId = peerDiscussionId + if let peerDiscussionId = peerDiscussionId { + strongSelf.preloadHistoryPeerIdDisposable.set(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId)) + } else { + strongSelf.preloadHistoryPeerIdDisposable.set(nil) + } + } + + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { + return $0.updatedPeer { _ in + return renderedPeer + }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages).updatedCurrentSendAsPeerId(currentSendAsPeerId) + .updatedCopyProtectionEnabled(copyProtectionEnabled) + .updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + + if let channel = renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } } - } - } else if let group = renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { + } else if let group = renderedPeer?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } } } + + return interfaceState } - - return interfaceState - } - }) + }) + } if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true strongSelf._chatLocationInfoReady.set(.single(true)) @@ -6314,6 +6453,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var presentationData = presentationData var useDarkAppearance = presentationData.theme.overallDarkAppearance + if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) { + let themeSettings: TelegramThemeSettings? + if let matching = theme.settings?.first(where: { $0.baseTheme == presentationData.theme.referenceTheme.baseTheme }) { + themeSettings = matching + } else { + themeSettings = theme.settings?.first + } + if let themeWallpaper = themeSettings?.wallpaper { + chatWallpaper = themeWallpaper + } + } if let themeEmoticon = themeEmoticon, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == themeEmoticon.strippedEmoji }) { if let darkAppearancePreview = darkAppearancePreview { useDarkAppearance = darkAppearancePreview @@ -6750,7 +6900,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatLocation: ChatLocation if let threadId { - chatLocation = .replyThread(message: ChatReplyThreadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + chatLocation = .replyThread(message: ChatReplyThreadMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) } else { chatLocation = .peer(id: peerId) } @@ -7055,7 +7205,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.addContentOffset(offset: offset, itemNode: itemNode) } + var closeOnEmpty = false if case .pinnedMessages = self.presentationInterfaceState.subject { + closeOnEmpty = true + } else if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + closeOnEmpty = true + } + + if closeOnEmpty { self.chatDisplayNode.historyNode.setLoadStateUpdated({ [weak self] state, _ in guard let strongSelf = self else { return @@ -7339,15 +7496,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let peerId = self.chatLocation.peerId { self.chatThemeEmoticonPromise.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThemeEmoticon(id: peerId))) - let chatWallpaper = self.context.account.viewTracker.peerView(peerId) + let chatWallpaper = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId)) |> take(1) - |> map { view -> TelegramWallpaper? in - if let cachedUserData = view.cachedData as? CachedUserData { - return cachedUserData.wallpaper - } else { - return nil - } - } self.chatWallpaperPromise.set(chatWallpaper) } else { self.chatThemeEmoticonPromise.set(.single(nil)) @@ -7495,6 +7645,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G themeEmoticon = cachedData.themeEmoticon } else if let cachedData = cachedData as? CachedChannelData { themeEmoticon = cachedData.themeEmoticon + chatWallpaper = cachedData.wallpaper } strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon)) @@ -8768,15 +8919,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) { - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).startStandalone() completion(.dismissWithoutContent) } else if (messages.first?.flags.isSending ?? false) { - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).startStandalone() completion(.dismissWithoutContent) } else { @@ -12429,7 +12574,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) case .replyThread: - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case let .replyThread(message) = self.chatLocation { + if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId.peerId == self.context.account.peerId { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case let .replyThread(message) = self.chatLocation { if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.inviteRequestsContext) { self.effectiveNavigationController?.pushViewController(infoController) } @@ -13456,7 +13605,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gift: let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in if let strongSelf = self { strongSelf.push(c) } @@ -17197,7 +17346,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let peerId = self.chatLocation.peerId else { return } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: peerId, updatedPresentationData: self.updatedPresentationData), navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in + let message = sourceMessageId.flatMap { self.chatDisplayNode.historyNode.messageInCurrentHistoryView($0) } + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: peerId, message: message, updatedPresentationData: self.updatedPresentationData), navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return } @@ -17402,9 +17552,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() } else if actions.contains(0) { - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if actions.contains(1) { @@ -17443,9 +17590,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) @@ -17470,7 +17614,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } - contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in + contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in if let strongSelf = self { var giveaway: TelegramMediaGiveaway? for messageId in messageIds { @@ -17483,9 +17627,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let commit = { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } if let giveaway { @@ -17498,8 +17639,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } f(.default) } else { - commit() - f(.dismissWithoutContent) + c.dismiss(completion: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + commit() + }) + }) } } }))) @@ -17507,9 +17651,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() } })) @@ -17533,29 +17674,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - c.dismiss(completion: { [weak strongSelf] in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { - guard let strongSelf else { - return - } - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() - }) + c.dismiss(completion: { [weak strongSelf] in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + guard let strongSelf else { + return + } + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() }) - } else { - f(.dismissWithoutContent) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() - } + }) } }))) items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.dustEffect { - strongSelf.chatDisplayNode.historyNode.setCurrentDeleteAnimationCorrelationIds(messageIds) - } let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() } @@ -18960,6 +19092,72 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: messageId) }) } + + func openStorySharing(messages: [Message]) { + let context = self.context + let subject: Signal = .single(.message(messages.map { $0.id })) + + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isPeerArchived: false, + transitionOut: nil + ) + + let controller = MediaEditorScreen( + context: context, + subject: subject, + transitionIn: nil, + transitionOut: { _, _ in + return nil + }, + completion: { [weak self] result, commit in + guard let self else { + return + } + let targetPeerId: EnginePeer.Id + let target: Stories.PendingTarget + if let sendAsPeerId = result.options.sendAsPeerId { + target = .peer(sendAsPeerId) + targetPeerId = sendAsPeerId + } else { + target = .myStories + targetPeerId = self.context.account.peerId + } + externalState.storyTarget = target + + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let text: String + if case .channel = peer { + text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string + } else { + text = self.presentationData.strings.Story_MessageReposted_Personal + } + Queue.mainQueue().after(0.25) { + self.present(UndoOverlayController( + presentationData: self.presentationData, + content: .forward(savedMessages: false, text: text), + elevatedLayout: false, + action: { _ in return false } + ), in: .current) + + Queue.mainQueue().after(0.1) { + self.chatDisplayNode.hapticFeedback.success() + } + } + }) + + } + ) + self.push(controller) + } } final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 8baa4336a3d..072d16ee0f6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -439,6 +439,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.titleAccessoryPanelContainer = ChatControllerTitlePanelNodeContainer() self.titleAccessoryPanelContainer.clipsToBounds = true + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) + self.inputContextPanelContainer = ChatControllerTitlePanelNodeContainer() self.inputContextOverTextPanelContainer = ChatControllerTitlePanelNodeContainer() @@ -1145,7 +1147,11 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer { - historyNodeContainer.isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + if historyNodeContainer.isSecret != isSecret { + historyNodeContainer.isSecret = isSecret + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) + } } var previousListBottomInset: CGFloat? @@ -3701,7 +3707,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var replyThreadId: Int64? if case let .replyThread(replyThreadMessage) = self.chatPresentationInterfaceState.chatLocation { - replyThreadId = Int64(replyThreadMessage.messageId.id) + replyThreadId = replyThreadMessage.threadId } for id in forwardMessageIds.sorted() { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 1f4552e6824..fff1dc8c19b 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -36,6 +36,7 @@ import ChatBotInfoItem import ChatMessageItem import ChatMessageItemImpl import ChatMessageItemView +import ChatMessageBubbleItemNode import ChatMessageTransitionNode import ChatControllerInteraction import DustEffect @@ -348,7 +349,8 @@ private func extractAssociatedData( translateToLanguage: String?, maxReadStoryId: Int32?, recommendedChannels: RecommendedChannels?, - audioTranscriptionTrial: AudioTranscription.TrialState + audioTranscriptionTrial: AudioTranscription.TrialState, + chatThemes: [TelegramTheme] ) -> ChatMessageItemAssociatedData { var automaticDownloadPeerId: EnginePeer.Id? var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel @@ -403,7 +405,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.messageId.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes) } private extension ChatHistoryLocationInput { @@ -702,6 +704,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var toLang: String? + private var allowDustEffect: Bool = true private var dustEffectLayer: DustEffectLayer? public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode = .bubbles, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) { @@ -722,8 +725,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.controllerInteraction = controllerInteraction self.mode = mode - if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_unread_alignment"] { - self.enableUnreadAlignment = false + if let data = context.currentAppConfiguration.with({ $0 }).data { + if let _ = data["ios_killswitch_disable_unread_alignment"] { + self.enableUnreadAlignment = false + } + if let _ = data["ios_killswitch_disable_dust_effect"] { + self.allowDustEffect = false + } } let presentationData = updatedPresentationData.initial @@ -922,6 +930,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom) } + var lastMessageId: MessageId? + if let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView { + if historyView.originalView.laterId == nil && !historyView.originalView.holeLater { + lastMessageId = historyView.originalView.entries.last?.message.id + } + } + var maxMessage: MessageIndex? strongSelf.forEachVisibleMessageItemNode { itemNode in if let item = itemNode.item { @@ -933,6 +948,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto matches = true } else if itemNode.frame.minY >= strongSelf.insets.top - 100.0 { matches = true + } else if let lastMessageId { + for (message, _) in item.content { + if message.id == lastMessageId { + matches = true + } + } } if matches { @@ -960,7 +981,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } if let maxMessage { - //print("read \(maxMessage.text)") strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxMessage) } } @@ -971,7 +991,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } |> distinctUntilChanged(isEqual: { $0 == $1 }) |> mapToSignal { messageId -> Signal in if let messageId = messageId { - return context.engine.messages.getMessagesLoadIfNecessary([messageId]) |> map { _ -> Void in return Void() } + return context.engine.messages.getMessagesLoadIfNecessary([messageId]) + |> `catch` { _ in + return .single(.result([])) + } + |> map { _ -> Void in return Void() } } else { return .complete() } @@ -1391,6 +1415,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto let audioTranscriptionTrial = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.AudioTranscriptionTrial()) + let chatThemes = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager) + let messageViewQueue = Queue.mainQueue() let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, @@ -1411,8 +1437,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto translationState, maxReadStoryId, recommendedChannels, - audioTranscriptionTrial - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial in + audioTranscriptionTrial, + chatThemes + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises func applyHole() { @@ -1568,7 +1595,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto translateToLanguage = languageCode } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, @@ -2985,22 +3012,16 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.hasActiveTransition = true let transition = self.enqueuedHistoryViewTransitions.removeFirst() - var expiredMessageIds = Set() + var expiredMessageStableIds = Set() if let previousHistoryView = self.historyView, transition.options.contains(.AnimateInsertion) { - let demoDustEffect = self.context.sharedContext.immediateExperimentalUISettings.dustEffect - - var existingIds = Set() + var existingStableIds = Set() for entry in transition.historyView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): - if message.autoremoveAttribute != nil || demoDustEffect { - existingIds.insert(message.id) - } + existingStableIds.insert(message.stableId) case let .MessageGroupEntry(_, messages, _): for message in messages { - if message.0.autoremoveAttribute != nil || demoDustEffect { - existingIds.insert(message.0.id) - } + existingStableIds.insert(message.0.stableId) } default: break @@ -3010,27 +3031,32 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto for entry in previousHistoryView.filteredEntries { switch entry { case let .MessageEntry(message, _, _, _, _, _): - if !existingIds.contains(message.id) { + if !existingStableIds.contains(message.stableId) { if let autoremoveAttribute = message.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime { let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout if exipiresAt >= currentTimestamp - 1 { - expiredMessageIds.insert(message.id) + expiredMessageStableIds.insert(message.stableId) } - } else if demoDustEffect { - expiredMessageIds.insert(message.id) + } else { + //expiredMessageStableIds.insert(message.stableId) } } case let .MessageGroupEntry(_, messages, _): - for message in messages { - if !existingIds.contains(message.0.id) { - if let autoremoveAttribute = message.0.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime { - let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout - if exipiresAt >= currentTimestamp - 1 { - expiredMessageIds.insert(message.0.id) - } else if demoDustEffect { - expiredMessageIds.insert(message.0.id) - } + var isRemoved = true + inner: for message in messages { + if existingStableIds.contains(message.0.stableId) { + isRemoved = false + break inner + } + } + if isRemoved, let message = messages.first?.0 { + if let autoremoveAttribute = message.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime { + let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout + if exipiresAt >= currentTimestamp - 1 { + expiredMessageStableIds.insert(message.stableId) } + } else { + //expiredMessageStableIds.insert(message.stableId) } } default: @@ -3038,17 +3064,23 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } } - self.currentDeleteAnimationCorrelationIds.formUnion(expiredMessageIds) + self.currentDeleteAnimationCorrelationIds.formUnion(expiredMessageStableIds) - var appliedDeleteAnimationCorrelationIds = Set() - if !self.currentDeleteAnimationCorrelationIds.isEmpty { + var appliedDeleteAnimationCorrelationIds = Set() + if !self.currentDeleteAnimationCorrelationIds.isEmpty && self.allowDustEffect { var foundItemNodes: [ChatMessageItemView] = [] self.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { for (message, _) in item.content { - if self.currentDeleteAnimationCorrelationIds.contains(message.id) { - appliedDeleteAnimationCorrelationIds.insert(message.id) - self.currentDeleteAnimationCorrelationIds.remove(message.id) + if let itemNode = itemNode as? ChatMessageBubbleItemNode { + if itemNode.isServiceLikeMessage() { + continue + } + } + + if self.currentDeleteAnimationCorrelationIds.contains(message.stableId) { + appliedDeleteAnimationCorrelationIds.insert(message.stableId) + self.currentDeleteAnimationCorrelationIds.remove(message.stableId) foundItemNodes.append(itemNode) } } @@ -3306,7 +3338,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if let messageIndex = messageIndex { let _ = messageIndex - //strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) } } } @@ -4094,11 +4125,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.currentSendAnimationCorrelationIds = value } - private var currentDeleteAnimationCorrelationIds = Set() - func setCurrentDeleteAnimationCorrelationIds(_ value: Set) { + private var currentDeleteAnimationCorrelationIds = Set() + func setCurrentDeleteAnimationCorrelationIds(_ value: Set) { self.currentDeleteAnimationCorrelationIds = value } - private var currentAppliedDeleteAnimationCorrelationIds = Set() + private var currentAppliedDeleteAnimationCorrelationIds = Set() var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)? @@ -4198,8 +4229,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if !self.currentAppliedDeleteAnimationCorrelationIds.isEmpty { if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { for (message, _) in item.content { - if self.currentAppliedDeleteAnimationCorrelationIds.contains(message.id) { - return 1.5 + if self.currentAppliedDeleteAnimationCorrelationIds.contains(message.stableId) { + return 0.8 } } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 7ffad99761a..4473825f084 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1,6 +1,3 @@ -// MARK: Nicegram StickerMaker -import FeatPartners -// import Foundation import UIKit import Postbox @@ -181,6 +178,9 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo } else if let _ = media as? TelegramMediaGiveaway { hasUneditableAttributes = true break + } else if let _ = media as? TelegramMediaGiveawayResults { + hasUneditableAttributes = true + break } } @@ -1897,20 +1897,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controller.setItems(.single(ContextController.Items(content: .list(ngContextItems))), minHeight: nil, animated: true) }))) - - // MARK: Nicegram StickerMaker - if data.messageActions.options.contains(.viewStickerPack), - StickerMaker.showConfig.contextMenu { - actions.append(.action(ContextMenuActionItem(text: StickerMaker.buttonTitle, icon: { theme in - return generateTintedImage(image: StickerMaker.contextMenuIcon, color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - StickerMaker.open( - from: .contextMenu - ) - f(.dismissWithoutContent) - }))) - } - // // MARK: Nicegram SelectAllMessagesWithAuthor if let authorId = message.author?.id { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 045bc803f39..d55673ba542 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -538,7 +538,7 @@ struct UrlPreviewState { var detectedUrls: [String] } -func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: UrlPreviewState?) -> (UrlPreviewState?, Signal<(TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?, NoError>)? { +func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: UrlPreviewState?, forPeerId: PeerId?) -> (UrlPreviewState?, Signal<(TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?, NoError>)? { guard let _ = inputText else { if currentQuery != nil { return (nil, .single({ _ in return nil })) @@ -550,7 +550,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco let detectedUrls = detectUrls(inputText) if detectedUrls != (currentQuery?.detectedUrls ?? []) { if !detectedUrls.isEmpty { - return (UrlPreviewState(detectedUrls: detectedUrls), webpagePreview(account: context.account, urls: detectedUrls) + return (UrlPreviewState(detectedUrls: detectedUrls), webpagePreview(account: context.account, urls: detectedUrls, forPeerId: forPeerId) |> mapToSignal { result -> Signal<(TelegramMediaWebpage, String)?, NoError> in guard case let .result(webpageResult) = result else { return .complete() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index f79b9047726..5d9a9bae6d8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -126,6 +126,17 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } } + if case let .replyThread(message) = chatPresentationInterfaceState.chatLocation, message.messageId.peerId == context.account.peerId { + if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { + return (currentPanel, nil) + } else { + let panel = ChatChannelSubscriberInputPanelNode() + panel.interfaceInteraction = interfaceInteraction + panel.context = context + return (panel, nil) + } + } + if let secretChat = peer as? TelegramSecretChat { switch secretChat.embeddedState { case .handshake: diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index ea75b733643..90475e41d26 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -57,7 +57,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha return nil } -func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?, moreInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { +func rightNavigationButtonForChatInterfaceState(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?, moreInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { if let _ = presentationInterfaceState.interfaceState.selectionState { if case .messageOptions = presentationInterfaceState.subject { return nil @@ -71,6 +71,10 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } } + if case let .replyThread(message) = presentationInterfaceState.chatLocation, message.messageId.peerId == context.account.peerId { + return chatInfoNavigationButton + } + if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), let moreInfoNavigationButton = moreInfoNavigationButton { if case .replyThread = presentationInterfaceState.chatLocation { } else { diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index d04a66cebbe..5f758642c53 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -540,6 +540,11 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { } else { emojiStatus = emojiStatusValue } + } else if let channel = interfaceState.renderedPeer?.peer as? TelegramChannel, let emojiStatusValue = channel.emojiStatus { + if channel.isFake || channel.isScam { + } else { + emojiStatus = emojiStatusValue + } } /*#if DEBUG diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index a14f0dc79ca..5b124d6689d 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -266,11 +266,13 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { + }, openPremiumGift: { }, openActiveSessions: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/ComposeControllerNode.swift b/submodules/TelegramUI/Sources/ComposeControllerNode.swift index 380d2f4ec87..897ad343132 100644 --- a/submodules/TelegramUI/Sources/ComposeControllerNode.swift +++ b/submodules/TelegramUI/Sources/ComposeControllerNode.swift @@ -51,7 +51,7 @@ final class ComposeControllerNode: ASDisplayNode { ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: .generic(UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")!), action: { openCreateNewChannelImpl?() }) - ], includeChatList: false)), displayPermissionPlaceholder: false) + ], includeChatList: false, topPeers: false)), displayPermissionPlaceholder: false) super.init() diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 888cc249347..d89f9bb1a6c 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -201,6 +201,20 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton rightNavigationButton.isEnabled = true //count != 0 || self.params.alwaysEnabled + case .premiumGifting: + let maxCount: Int32 = self.limit ?? 10 + var count = 0 + if case let .contacts(contactsNode) = self.contactsNode.contentNode { + count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 + } + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") + case .requestedUsersSelection: + let maxCount: Int32 = self.limit ?? 10 + var count = 0 + if case let .contacts(contactsNode) = self.contactsNode.contentNode { + count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 + } + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.RequestPeer_SelectUsers, counter: "\(count)/\(maxCount)") case .channelCreation: self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "") let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) @@ -250,11 +264,20 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection var displayCountAlert = false var selectionState: ContactListNodeGroupSelectionState? + let reachedLimit = strongSelf.params.reachedLimit switch strongSelf.contactsNode.contentNode { case let .contacts(contactsNode): contactsNode.updateSelectionState { state in if let state = state { var updatedState = state.withToggledPeerId(.peer(peer.id)) + if let limit = limit, updatedState.selectedPeerIndices.count > limit { + reachedLimit?(Int32(limit)) + updatedCount = nil + removedTokenId = nil + addedToken = nil + return state + } + if updatedState.selectedPeerIndices[.peer(peer.id)] == nil { removedTokenId = peer.id } else { @@ -273,7 +296,6 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } case let .chats(chatsNode): - let reachedLimit = strongSelf.params.reachedLimit chatsNode.updateState { initialState in var state = initialState if state.selectedPeerIds.contains(peer.id) { @@ -310,7 +332,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection switch strongSelf.mode { case .groupCreation, .peerSelection, .chatSelection: strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled - case .channelCreation: + case .channelCreation, .premiumGifting, .requestedUsersSelection: break } @@ -318,6 +340,12 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection case .groupCreation: let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") + case .premiumGifting: + let maxCount: Int32 = strongSelf.limit ?? 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") + case .requestedUsersSelection: + let maxCount: Int32 = strongSelf.limit ?? 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.RequestPeer_SelectUsers, counter: "\(updatedCount)/\(maxCount)") case .peerSelection, .channelCreation, .chatSelection: break } @@ -389,13 +417,19 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection switch strongSelf.mode { case .groupCreation, .peerSelection, .chatSelection: strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled - case .channelCreation: + case .channelCreation, .premiumGifting, .requestedUsersSelection: break } switch strongSelf.mode { case .groupCreation: let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") + case .premiumGifting: + let maxCount: Int32 = strongSelf.limit ?? 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") + case .requestedUsersSelection: + let maxCount: Int32 = strongSelf.limit ?? 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.RequestPeer_SelectUsers, counter: "\(updatedCount)/\(maxCount)") case .peerSelection, .channelCreation, .chatSelection: break } @@ -500,11 +534,30 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } self.contactsNode.complete = { [weak self] in - if let strongSelf = self, let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem, rightBarButtonItem.isEnabled { - strongSelf.rightNavigationButtonPressed() + if let strongSelf = self { + var available = true + if let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem { + available = rightBarButtonItem.isEnabled + } + if available { + strongSelf.rightNavigationButtonPressed() + } } } + switch self.contactsNode.contentNode { + case let .contacts(contactsNode): + contactsNode.deselectedAll = { [weak self] in + guard let self else { + return + } + self.contactsNode.editableTokens = [] + self.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)) + } + case .chats: + break + } + self.displayNodeDidLoad() } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 725d1259e5e..e09805422fe 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -12,6 +12,7 @@ import ChatListUI import AnimationCache import MultiAnimationRenderer import EditableTokenListNode +import SolidRoundedButtonNode private struct SearchResultEntry: Identifiable { let index: Int @@ -72,6 +73,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer + private let footerPanelNode: FooterPanelNode? + private let isPeerEnabled: ((EnginePeer) -> Bool)? init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer) -> Void)?, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) { @@ -85,18 +88,34 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.isPeerEnabled = isPeerEnabled + var proceedImpl: (() -> Void)? + var placeholder: String + var shortPlaceholder: String? var includeChatList = false switch mode { - case let .peerSelection(_, searchGroups, searchChannels): - includeChatList = searchGroups || searchChannels - if searchGroups { - placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel - } else { - placeholder = self.presentationData.strings.Contacts_SearchLabel - } - default: - placeholder = self.presentationData.strings.Compose_TokenListPlaceholder + case let .peerSelection(_, searchGroups, searchChannels): + includeChatList = searchGroups || searchChannels + if searchGroups { + placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel + } else { + placeholder = self.presentationData.strings.Contacts_SearchLabel + } + self.footerPanelNode = nil + case .premiumGifting: + placeholder = self.presentationData.strings.Premium_Gift_ContactSelection_Placeholder + shortPlaceholder = self.presentationData.strings.Common_Search + self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { + proceedImpl?() + }) + case .requestedUsersSelection: + placeholder = self.presentationData.strings.RequestPeer_SelectUsers_SearchPlaceholder + self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { + proceedImpl?() + }) + default: + placeholder = self.presentationData.strings.Compose_TokenListPlaceholder + self.footerPanelNode = nil } if case let .chatSelection(chatSelection) = mode { @@ -132,10 +151,16 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } self.contentNode = .chats(chatListNode) } else { - self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState())) + var displayTopPeers = false + if case .premiumGifting = mode { + displayTopPeers = true + } else if case .requestedUsersSelection = mode { + displayTopPeers = true + } + self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, selectionState: ContactListNodeGroupSelectionState())) } - self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder) + self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder, shortPlaceholder: shortPlaceholder) super.init() @@ -215,6 +240,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchGroups = true searchChannels = true globalSearch = false + case .premiumGifting, .requestedUsersSelection: + searchChatList = true } let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch)), filters: filters, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _ in @@ -248,6 +275,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.tokenListNode.textReturned = { [weak self] in self?.complete?() } + + if let footerPanelNode = self.footerPanelNode { + proceedImpl = { [weak self] in + self?.complete?() + } + self.addSubnode(footerPanelNode) + } } deinit { @@ -284,6 +318,21 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { insets.top += tokenListHeight headerInsets.top += tokenListHeight + if let footerPanelNode = self.footerPanelNode { + var count = 0 + if case let .contacts(contactListNode) = self.contentNode { + count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0 + } + footerPanelNode.count = count + let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: headerInsets.bottom, transition: transition) + if count == 0 { + transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight))) + } else { + insets.bottom += panelHeight + transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + } + } + switch self.contentNode { case let .contacts(contactsNode): contactsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) @@ -318,3 +367,68 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { }) } } + + +private final class FooterPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let separatorNode: ASDisplayNode + private let button: SolidRoundedButtonView + + private var validLayout: (CGFloat, CGFloat, CGFloat)? + + var count: Int = 0 { + didSet { + if self.count != oldValue && self.count > 0 { + self.button.title = self.strings.Premium_Gift_ContactSelection_Proceed + self.button.badge = "\(self.count)" + + if let (width, sideInset, bottomInset) = self.validLayout { + let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) + } + } + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor + + self.addSubnode(self.separatorNode) + + self.button.pressed = { + action() + } + } + + override func didLoad() { + super.didLoad() + self.view.addSubview(self.button) + } + + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 9.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) + + let buttonInset: CGFloat = 16.0 + sideInset + let buttonWidth = width - buttonInset * 2.0 + let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) + transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + return topInset + buttonHeight + bottomInset + } +} diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index ad60e31dd4e..762797102ad 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.filters = filters var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false, topPeers: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection) diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index b6f83c315ee..f181e3c929e 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -18,6 +18,8 @@ import StoryContainerScreen import CameraScreen import MediaEditorScreen import ChatControllerInteraction +import SavedMessagesScreen +import WallpaperGalleryScreen public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { if case let .peer(peer) = params.chatLocation { @@ -83,6 +85,13 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam return } + /*if case let .peer(peer) = params.chatLocation, peer.id == params.context.account.peerId { + let savedMessagesScreen = SavedMessagesScreen(context: params.context) + params.navigationController.pushViewController(savedMessagesScreen, completion: { + }) + return + }*/ + var found = false var isFirst = true if params.useExisting { diff --git a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift index 512dcc5f80f..52e0cb6ff26 100644 --- a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift +++ b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift @@ -84,6 +84,15 @@ class NGDeeplinkHandler { } case "pstAuth": return handlePstAuth(url: url) + case "refferaldraw": + if #available(iOS 15.0, *) { + Task { @MainActor in + AssistantUITgHelper.showReferralDrawFromDeeplink() + } + return true + } else { + return false + } case "task": if #available(iOS 15.0, *) { let taskDeeplinkHandler = TasksContainer.shared.taskDeeplinkHandler() @@ -161,11 +170,9 @@ private extension NGDeeplinkHandler { } func handleOnboarding(url: URL) -> Bool { - let presentationData = getCurrentPresentationData() - var dismissImpl: (() -> Void)? - let c = onboardingController(languageCode: presentationData.strings.baseLanguageCode) { + let c = onboardingController() { dismissImpl?() } c.modalPresentationStyle = .fullScreen diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index fb5028708a5..f84afb561ef 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -23,6 +23,7 @@ import UndoUI import WebsiteType import GalleryData import StoryContainerScreen +import WallpaperGalleryScreen func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { var story: TelegramMediaStory? diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index cba989a3384..d7ba72c9457 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -31,6 +31,7 @@ import PremiumUI import AuthorizationUI import ChatFolderLinkPreviewScreen import StoryContainerScreen +import WallpaperGalleryScreen private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -66,7 +67,7 @@ func openResolvedUrlImpl( completion: (() -> Void)? ) { let updatedPresentationData: (initial: PresentationData, signal: Signal)? - if case let .chat(_, maybeUpdatedPresentationData) = urlContext { + if case let .chat(_, _, maybeUpdatedPresentationData) = urlContext { updatedPresentationData = maybeUpdatedPresentationData } else { updatedPresentationData = nil @@ -674,7 +675,7 @@ func openResolvedUrlImpl( navigationController.pushViewController(controller) } } else { - if case let .chat(chatPeerId, _) = urlContext { + if case let .chat(chatPeerId, _, _) = urlContext { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) |> deliverOnMainQueue).start(next: { chatPeer in guard let navigationController = navigationController, let chatPeer else { @@ -725,7 +726,7 @@ func openResolvedUrlImpl( navigationController.pushViewController(controller) } } else { - if case let .chat(chatPeerId, _) = urlContext { + if case let .chat(chatPeerId, _, _) = urlContext { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) |> deliverOnMainQueue).start(next: { chatPeer in guard let navigationController = navigationController, let chatPeer else { @@ -860,7 +861,7 @@ func openResolvedUrlImpl( }) case let .boost(peerId, status, myBoostStatus): var isCurrent = false - if case let .chat(chatPeerId, _) = urlContext, chatPeerId == peerId { + if case let .chat(chatPeerId, _, _) = urlContext, chatPeerId == peerId { isCurrent = true } var forceDark = false @@ -935,70 +936,81 @@ func openResolvedUrlImpl( let _ = (signal |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] giftCode in if let giftCode { - var dismissImpl: (() -> Void)? - let controller = PremiumGiftCodeScreen( - context: context, - subject: .giftCode(giftCode), - forceDark: forceDark, - action: { [weak navigationController] in - let _ = (context.engine.payments.applyPremiumGiftCode(slug: slug) - |> deliverOnMainQueue).startStandalone(completed: { - dismissImpl?() - - let controller = PremiumIntroScreen(context: context, source: .settings, forceDark: forceDark, forceHasPremium: true) - navigationController?.pushViewController(controller) - Queue.mainQueue().after(0.3, { - controller.animateSuccess() - }) - }) - }, - openPeer: { peer in - if peer.id != context.account.peerId { - openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) - if case let .chat(peerId, _) = urlContext, peerId == peer.id { - dismissImpl?() - } - } - }, - openMessage: { messageId in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - guard let peer else { - return - } - openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), peekData: nil)) - if case let .chat(peerId, _) = urlContext, peerId == messageId.peerId { + if giftCode.fromPeerId == nil, let toPeerId = giftCode.toPeerId { + let fromPeerId: PeerId + if case let .chat(peerId, message, _) = urlContext { + fromPeerId = message?.author?.id ?? peerId + } else { + fromPeerId = context.account.peerId + } + let controller = PremiumIntroScreen(context: context, source: .gift(from: fromPeerId, to: toPeerId, duration: giftCode.months, giftCode: giftCode)) + navigationController?.pushViewController(controller) + } else { + var dismissImpl: (() -> Void)? + let controller = PremiumGiftCodeScreen( + context: context, + subject: .giftCode(giftCode), + forceDark: forceDark, + action: { [weak navigationController] in + let _ = (context.engine.payments.applyPremiumGiftCode(slug: slug) + |> deliverOnMainQueue).startStandalone(completed: { dismissImpl?() + + let controller = PremiumIntroScreen(context: context, source: .settings, forceDark: forceDark, forceHasPremium: true) + navigationController?.pushViewController(controller) + Queue.mainQueue().after(0.3, { + controller.animateSuccess() + }) + }) + }, + openPeer: { peer in + if peer.id != context.account.peerId { + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + if case let .chat(peerId, _, _) = urlContext, peerId == peer.id { + dismissImpl?() + } } - }) - }, - shareLink: { link in - let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - - let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true)) - peerSelectionController.peerSelected = { [weak peerSelectionController] peer, threadId in - if let _ = peerSelectionController { - Queue.mainQueue().after(0.88) { - HapticFeedback().success() + }, + openMessage: { messageId in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer else { + return } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) - - let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages) - |> deliverOnMainQueue).startStandalone() - if let peerSelectionController = peerSelectionController { - peerSelectionController.dismiss() + openPeer(peer, .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), peekData: nil)) + if case let .chat(peerId, _, _) = urlContext, peerId == messageId.peerId { + dismissImpl?() + } + }) + }, + shareLink: { link in + let messages: [EnqueueMessage] = [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + + let peerSelectionController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], multipleSelection: false, selectForumThreads: true)) + peerSelectionController.peerSelected = { [weak peerSelectionController] peer, threadId in + if let _ = peerSelectionController { + Queue.mainQueue().after(0.88) { + HapticFeedback().success() + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + (navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: peer.id == context.account.peerId ? presentationData.strings.GiftLink_LinkSharedToSavedMessages : presentationData.strings.GiftLink_LinkSharedToChat(peer.compactDisplayTitle).string), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .window(.root)) + + let _ = (enqueueMessages(account: context.account, peerId: peer.id, messages: messages) + |> deliverOnMainQueue).startStandalone() + if let peerSelectionController = peerSelectionController { + peerSelectionController.dismiss() + } } } + navigationController?.pushViewController(peerSelectionController) } - navigationController?.pushViewController(peerSelectionController) + ) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() } - ) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() + navigationController?.pushViewController(controller) } - navigationController?.pushViewController(controller) } else { present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 61cddab88ee..a9982911069 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -166,7 +166,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in - }, openRequestedPeerSelection: { _, _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 65a6ea64892..fbb7cf19cb6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -50,6 +50,7 @@ import ChatMessageItemImpl import ChatRecentActionsController import PeerInfoScreen import ChatQrCodeScreen +import UndoUI import NGCore import NGData @@ -136,8 +137,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { private var groupCallDisposable: Disposable? private var callController: CallController? + private var call: PresentationCall? public let hasOngoingCall = ValuePromise(false) private let callState = Promise(nil) + private var awaitingCallConnectionDisposable: Disposable? private var groupCallController: VoiceChatController? public var currentGroupCallController: ViewController? { @@ -315,17 +318,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.mediaManager = MediaManagerImpl(accountManager: accountManager, inForeground: applicationBindings.applicationInForeground, presentationData: presentationData) - // MARK: Nicegram Themes - if #available(iOS 13.0, *) { - _ = (presentationData - |> deliverOnMainQueue) - .start(next: { presentationData in - let isDark = presentationData.theme.overallDarkAppearance - UIApplication.findKeyWindow()?.overrideUserInterfaceStyle = isDark ? .dark : .light - }) - } - // - self.mediaManager.overlayMediaManager.updatePossibleEmbeddingItem = { [weak self] item in guard let strongSelf = self else { return @@ -394,6 +386,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { } if themeUpdated { updateLegacyTheme() + + /*if #available(iOS 13.0, *) { + let userInterfaceStyle: UIUserInterfaceStyle + if strongSelf.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { + userInterfaceStyle = .dark + } else { + userInterfaceStyle = .light + } + if let eventView = strongSelf.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { + eventView.overrideUserInterfaceStyle = userInterfaceStyle + } + }*/ } if themeNameUpdated { strongSelf.presentCrossfadeController() @@ -782,26 +786,49 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable = (callManager.currentCallSignal |> deliverOnMainQueue).start(next: { [weak self] call in - if let strongSelf = self { - if call !== strongSelf.callController?.call { - strongSelf.callController?.dismiss() - strongSelf.callController = nil - strongSelf.hasOngoingCall.set(false) + guard let self else { + return + } + + if call !== self.call { + self.call = call + + self.callController?.dismiss() + self.callController = nil + self.hasOngoingCall.set(false) + + if let call { + self.callState.set(call.state + |> map(Optional.init)) + self.hasOngoingCall.set(true) + setNotificationCall(call) - if let call = call { - mainWindow.hostView.containerView.endEditing(true) - let callController = CallController(sharedContext: strongSelf, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) - strongSelf.callController = callController - strongSelf.mainWindow?.present(callController, on: .calls) - strongSelf.callState.set(call.state - |> map(Optional.init)) - strongSelf.hasOngoingCall.set(true) - setNotificationCall(call) - } else { - strongSelf.callState.set(.single(nil)) - strongSelf.hasOngoingCall.set(false) - setNotificationCall(nil) + if !call.isOutgoing && call.isIntegratedWithCallKit { + self.awaitingCallConnectionDisposable = (call.state + |> filter { state in + switch state.state { + case .ringing: + return false + default: + return true + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.presentControllerWithCurrentCall() + }) + } else{ + self.presentControllerWithCurrentCall() } + } else { + self.callState.set(.single(nil)) + self.hasOngoingCall.set(false) + self.awaitingCallConnectionDisposable?.dispose() + self.awaitingCallConnectionDisposable = nil + setNotificationCall(nil) } } }) @@ -1018,6 +1045,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } }) + + /*if #available(iOS 13.0, *) { + let userInterfaceStyle: UIUserInterfaceStyle + if self.currentPresentationData.with({ $0 }).theme.overallDarkAppearance { + userInterfaceStyle = .dark + } else { + userInterfaceStyle = .light + } + if let eventView = self.mainWindow?.hostView.eventView, eventView.overrideUserInterfaceStyle != userInterfaceStyle { + eventView.overrideUserInterfaceStyle = userInterfaceStyle + } + }*/ } deinit { @@ -1032,9 +1071,11 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() + self.awaitingCallConnectionDisposable?.dispose() // MARK: Nicegram DB Changes self.activeAccountsSettingsDisposable?.dispose() self.applicationInForegroundDisposable?.dispose() + // } private var didPerformAccountSettingsImport = false @@ -1094,6 +1135,37 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } + private func presentControllerWithCurrentCall() { + guard let call = self.call else { + return + } + + if let currentCallController = self.callController { + if currentCallController.call === call { + self.navigateToCurrentCall() + return + } else { + self.callController = nil + currentCallController.dismiss() + } + } + + self.mainWindow?.hostView.containerView.endEditing(true) + let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild) + self.callController = callController + callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in + guard let self, let callController else { + completion(false) + return + } + if callController.window == nil { + self.mainWindow?.present(callController, on: .calls) + } + completion(true) + } + self.mainWindow?.present(callController, on: .calls) + } + public func updateNotificationTokensRegistration() { let sandbox: Bool #if DEBUG @@ -1731,7 +1803,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in - }, openRequestedPeerSelection: { _, _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in @@ -1766,6 +1838,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return ChatMessageDateHeader(timestamp: timestamp, scheduled: false, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), controllerInteraction: nil, context: context) } + public func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { + return ChatMessageAvatarHeader(timestamp: timestamp, peerId: peer.id, peer: peer, messageReference: nil, message: message, presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, controllerInteraction: nil, storyStats: nil) + } + public func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = legacyWallpaperPicker(context: context, presentationData: presentationData).start(next: { generator in @@ -2006,6 +2082,72 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } + public func makePremiumGiftController(context: AccountContext) -> ViewController { + let options = Promise<[PremiumGiftCodeOption]>() + options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let limit: Int32 = 10 + var reachedLimitImpl: ((Int32) -> Void)? + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .premiumGifting, options: [], isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil { + return true + } else { + return false + } + }, limit: limit, reachedLimit: { limit in + reachedLimitImpl?(limit) + })) + + reachedLimitImpl = { [weak controller] limit in + guard let controller else { + return + } + HapticFeedback().error() + controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + + let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get()) + .startStandalone(next: { [weak controller] result, options in + guard let controller else { + return + } + var peerIds: [PeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }) + } + + let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + var pushImpl: ((ViewController) -> Void)? + var filterImpl: (() -> Void)? + let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: .settings, pushController: { c in + pushImpl?(c) + }, completion: { + filterImpl?() + }) + pushImpl = { [weak giftController] c in + giftController?.push(c) + } + filterImpl = { [weak giftController] in + if let navigationController = giftController?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactMultiselectionController) } + navigationController.setViewControllers(controllers, animated: true) + } + } + controller.push(giftController) + }) + + return controller + } + public func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } @@ -2059,6 +2201,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var reactionSourceMessageId: MessageId? var callMessages: [Message] = [] var hintGroupInCommon: PeerId? + var forumTopicThread: ChatReplyThreadMessage? switch mode { case let .nearbyPeer(distance): @@ -2071,12 +2214,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation hintGroupInCommon = id case let .reaction(messageId): reactionSourceMessageId = messageId - case .forumTopic: - break + case let .forumTopic(thread): + forumTopicThread = thread default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread) } else if peer is TelegramSecretChat { return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: []) } diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index b02831f29b3..e925d46e541 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -92,7 +92,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) case .boost, .chatFolder: if let navigationController = controller.navigationController as? NavigationController { - openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in + openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in openResolvedPeerImpl(peer, navigateToPeer) }, sendFile: nil, sendSticker: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) } diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 39f77c11ae8..b935dc4a6eb 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -54,8 +54,8 @@ public struct ExperimentalUISettings: Codable, Equatable { public var storiesJpegExperiment: Bool public var crashOnMemoryPressure: Bool public var unidirectionalSwipeToReply: Bool - public var dustEffect: Bool - public var callUIV2: Bool + public var callV2: Bool + public var alternativeStoryMedia: Bool public var allowWebViewInspection: Bool public static var defaultSettings: ExperimentalUISettings { @@ -90,8 +90,8 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesJpegExperiment: false, crashOnMemoryPressure: false, unidirectionalSwipeToReply: false, - dustEffect: false, - callUIV2: false, + callV2: false, + alternativeStoryMedia: false, allowWebViewInspection: false ) } @@ -126,8 +126,8 @@ public struct ExperimentalUISettings: Codable, Equatable { storiesJpegExperiment: Bool, crashOnMemoryPressure: Bool, unidirectionalSwipeToReply: Bool, - dustEffect: Bool, - callUIV2: Bool, + callV2: Bool, + alternativeStoryMedia: Bool, allowWebViewInspection: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack @@ -159,8 +159,8 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesJpegExperiment = storiesJpegExperiment self.crashOnMemoryPressure = crashOnMemoryPressure self.unidirectionalSwipeToReply = unidirectionalSwipeToReply - self.dustEffect = dustEffect - self.callUIV2 = callUIV2 + self.callV2 = callV2 + self.alternativeStoryMedia = alternativeStoryMedia self.allowWebViewInspection = allowWebViewInspection } @@ -196,8 +196,8 @@ public struct ExperimentalUISettings: Codable, Equatable { self.storiesJpegExperiment = try container.decodeIfPresent(Bool.self, forKey: "storiesJpegExperiment") ?? false self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false self.unidirectionalSwipeToReply = try container.decodeIfPresent(Bool.self, forKey: "unidirectionalSwipeToReply") ?? false - self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect_2") ?? false - self.callUIV2 = try container.decodeIfPresent(Bool.self, forKey: "callUIV2") ?? false + self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false + self.alternativeStoryMedia = try container.decodeIfPresent(Bool.self, forKey: "alternativeStoryMedia") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false } @@ -233,8 +233,8 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.storiesJpegExperiment, forKey: "storiesJpegExperiment") try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure") try container.encode(self.unidirectionalSwipeToReply, forKey: "unidirectionalSwipeToReply") - try container.encode(self.dustEffect, forKey: "dustEffect_2") - try container.encode(self.callUIV2, forKey: "callUIV2") + try container.encode(self.callV2, forKey: "callV2") + try container.encode(self.alternativeStoryMedia, forKey: "alternativeStoryMedia") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index b165172c2c6..5fa784ff84a 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -277,16 +277,21 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case topicInfo } + public enum Custom: Codable { + case topic(id: Int64, info: EngineMessageHistoryThread.Info) + case nameColors([UInt32]) + } + public let interactivelySelectedFromPackId: ItemCollectionId? public let fileId: Int64 public let file: TelegramMediaFile? - public let topicInfo: (Int64, EngineMessageHistoryThread.Info)? + public let custom: Custom? - public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, topicInfo: (Int64, EngineMessageHistoryThread.Info)? = nil) { + public init(interactivelySelectedFromPackId: ItemCollectionId?, fileId: Int64, file: TelegramMediaFile?, custom: Custom? = nil) { self.interactivelySelectedFromPackId = interactivelySelectedFromPackId self.fileId = fileId self.file = file - self.topicInfo = topicInfo + self.custom = custom super.init() } @@ -296,11 +301,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { self.interactivelySelectedFromPackId = try container.decodeIfPresent(ItemCollectionId.self, forKey: .interactivelySelectedFromPackId) self.fileId = try container.decode(Int64.self, forKey: .fileId) self.file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) - if let topicId = try container.decodeIfPresent(Int64.self, forKey: .topicId), let topicInfo = try container.decodeIfPresent(EngineMessageHistoryThread.Info.self, forKey: .topicInfo) { - self.topicInfo = (topicId, topicInfo) - } else { - self.topicInfo = nil - } + self.custom = nil } public func encode(to encoder: Encoder) throws { @@ -308,16 +309,11 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { try container.encodeIfPresent(self.interactivelySelectedFromPackId, forKey: .interactivelySelectedFromPackId) try container.encode(self.fileId, forKey: .fileId) try container.encodeIfPresent(self.file, forKey: .file) - if let (topicId, topicInfo) = self.topicInfo { - try container.encode(topicId, forKey: .topicId) - try container.encode(topicInfo, forKey: .topicInfo) - } } override public func isEqual(_ object: Any?) -> Bool { if let other = object as? ChatTextInputTextCustomEmojiAttribute { return self === other - //return self.stickerPack == other.stickerPack && self.fileId == other.fileId && self.file?.fileId == other.file?.fileId } else { return false } diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index fa2e53f5da9..6b73742cdc1 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit fa2e53f5da9b9653ab47169a922fb6c82847134a +Subproject commit 6b73742cdc140c46a1ab1b8e3390354a9738e429 diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 26dfa5c535a..d63b3b5c3ea 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -15,7 +15,7 @@ public enum UndoOverlayContent { case info(title: String?, text: String, timeout: Double?, customUndoText: String?) case emoji(name: String, text: String) case swipeToReply(title: String, text: String) - case actionSucceeded(title: String, text: String, cancel: String, destructive: Bool) + case actionSucceeded(title: String?, text: String, cancel: String?, destructive: Bool) case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: StickerPackItem?, context: AccountContext) case dice(dice: TelegramMediaDice, context: AccountContext, text: String, action: String?) case chatAddedToFolder(chatTitle: String, folderTitle: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 17a66b81285..865ca15d614 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -262,10 +262,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + if let title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } self.textNode.attributedText = attributedText - displayUndo = true - undoText = cancel + if let cancel { + displayUndo = true + undoText = cancel + } else { + displayUndo = false + } self.originalRemainingSeconds = 5 case let .linkCopied(text): self.avatarNode = nil @@ -1341,7 +1347,9 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + if let title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } self.textNode.attributedText = attributedText default: break diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 385210f53d4..b6179adffd6 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -702,6 +702,9 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) if case let .channel(channel) = peer, channel.flags.contains(.isForum) { let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id) return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false)) + |> `catch` { _ in + return .single(.result([])) + } |> take(1) |> mapToSignal { result -> Signal in switch result { @@ -716,7 +719,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -741,7 +744,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessageId.id)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: replyId))) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: replyThreadMessageId.id)), threadId: Int64(replyThreadMessageId.id), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: replyId))) } else { return .result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -822,7 +825,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: Int64(threadId), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } @@ -830,6 +833,9 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } else { return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false)) + |> `catch` { _ in + return .single(.result([])) + } |> mapToSignal { result -> Signal in switch result { case .progress: @@ -843,7 +849,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .progress case let .result(info): if let _ = info { - return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) + return .result(.replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)) } else { return .result(.peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))) } diff --git a/submodules/Utils/LokiRng/Package.swift b/submodules/Utils/LokiRng/Package.swift index f2c2bcf28a1..db37e9f3847 100644 --- a/submodules/Utils/LokiRng/Package.swift +++ b/submodules/Utils/LokiRng/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "LokiRng", + platforms: [.macOS(.v10_13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/submodules/Utils/ShelfPack/Package.swift b/submodules/Utils/ShelfPack/Package.swift new file mode 100644 index 00000000000..9bd42def119 --- /dev/null +++ b/submodules/Utils/ShelfPack/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ShelfPack", + platforms: [.macOS(.v10_13)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "ShelfPack", + targets: ["ShelfPack"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "ShelfPack", + dependencies: [], + path: ".", + publicHeadersPath: "PublicHeaders"), + ], + cxxLanguageStandard: .cxx20 +) diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index fac8add04ef..a07d619330a 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1398,7 +1398,7 @@ private let messageImage: UIImage = { return messageBubbleImage(maxCornerRadius: 16.0, minCornerRadius: 16.0, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .none, shadow: nil, wallpaper: .color(0x000000), knockout: false) }() -public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false, message: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, channelMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false, message: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> var reference: MediaResourceReference? @@ -1452,7 +1452,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager 1 { c.clip() @@ -1748,7 +1756,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager 44.0 { + bottomInset = max(bottomInset, inputHeight) + } + let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - bottomInset))) if previousLayout != nil && (previousLayout?.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0, transition.isAnimated { webView.scrollToActiveElement(layout: layout, completion: { [weak self] contentOffset in diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index b3262d12621..ee016c6db03 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -3,6 +3,7 @@ import UIKit import Display import WebKit import SwiftSignalKit +import TelegramCore private let findActiveElementY = """ function getOffset(el) { @@ -91,8 +92,22 @@ function disconnectObserver() { final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } - init() { + init(account: Account) { let configuration = WKWebViewConfiguration() + + let uuid: UUID + + if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreUUID_\(account.id.int64)") as? String { + uuid = UUID(uuidString: current)! + } else { + uuid = UUID() + UserDefaults.standard.set(uuid.uuidString, forKey: "TelegramWebStoreUUID_\(account.id.int64)") + } + + if #available(iOS 17.0, *) { + configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid) + } + let contentController = WKUserContentController() var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? diff --git a/submodules/rlottie/LottieInstance.mm b/submodules/rlottie/LottieInstance.mm index 1d40bc2aab1..4989491f507 100755 --- a/submodules/rlottie/LottieInstance.mm +++ b/submodules/rlottie/LottieInstance.mm @@ -49,7 +49,9 @@ - (instancetype _Nullable)initWithData:(NSData * _Nonnull)data fitzModifier:(Lot } _frameCount = (int32_t)_animation->totalFrame(); + _frameCount = MAX(1, _frameCount); _frameRate = (int32_t)_animation->frameRate(); + _frameRate = MAX(1, _frameRate); size_t width = 0; size_t height = 0; @@ -59,6 +61,9 @@ - (instancetype _Nullable)initWithData:(NSData * _Nonnull)data fitzModifier:(Lot return nil; } + width = MAX(1, width); + height = MAX(1, height); + _dimensions = CGSizeMake(width, height); if ((_frameRate > 360) || _animation->duration() > 9.0) { diff --git a/swift_deps.bzl b/swift_deps.bzl index a6061575d5d..37e90e6d77a 100644 --- a/swift_deps.bzl +++ b/swift_deps.bzl @@ -36,7 +36,7 @@ def swift_dependencies(): # branch: develop swift_package( name = "swiftpkg_nicegram_assistant_ios", - commit = "05f2392cbf916a604d9d7e87dc640e7d4651dc12", + commit = "b0d993e6fcb6e48734d8361c26bd401752ac5513", dependencies_index = "@//:swift_deps_index.json", remote = "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", ) diff --git a/swift_deps_index.json b/swift_deps_index.json index b96aba3232f..db541e8613c 100644 --- a/swift_deps_index.json +++ b/swift_deps_index.json @@ -97,6 +97,16 @@ "nicegram-assistant" ] }, + { + "name": "FeatChatListBanner", + "c99name": "FeatChatListBanner", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:Sources_FeatChatListBanner", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "nicegram-assistant" + ] + }, { "name": "FeatImagesHub", "c99name": "FeatImagesHub", @@ -118,10 +128,10 @@ ] }, { - "name": "FeatPartners", - "c99name": "FeatPartners", + "name": "FeatPersistentStorage", + "c99name": "FeatPersistentStorage", "src_type": "swift", - "label": "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", + "label": "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPersistentStorage", "package_identity": "nicegram-assistant-ios", "product_memberships": [ "nicegram-assistant" @@ -338,6 +348,16 @@ "nicegram-assistant" ] }, + { + "name": "NGLocalization", + "c99name": "NGLocalization", + "src_type": "swift", + "label": "@swiftpkg_nicegram_assistant_ios//:Sources_NGLocalization", + "package_identity": "nicegram-assistant-ios", + "product_memberships": [ + "nicegram-assistant" + ] + }, { "name": "NGRepoTg", "c99name": "NGRepoTg", @@ -637,7 +657,6 @@ "target_labels": [ "@swiftpkg_nicegram_assistant_ios//:Sources_FeatChatBanner", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatImagesHubUI", - "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPartners", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPinnedChats", "@swiftpkg_nicegram_assistant_ios//:Sources_FeatPremiumUI", "@swiftpkg_nicegram_assistant_ios//:Sources_NGAiChatUI", @@ -808,7 +827,7 @@ "name": "swiftpkg_nicegram_assistant_ios", "identity": "nicegram-assistant-ios", "remote": { - "commit": "05f2392cbf916a604d9d7e87dc640e7d4651dc12", + "commit": "b0d993e6fcb6e48734d8361c26bd401752ac5513", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "branch": "develop" } diff --git a/versions.json b/versions.json index bcdcc881630..ca3424e1ccd 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.4.9", + "app": "1.5.0", "bazel": "6.4.0", "xcode": "15.0.1" }